summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gnu/local.mk2
-rw-r--r--gnu/packages/firmware.scm77
-rw-r--r--gnu/packages/patches/ergodox-firmware-fix-json-target.patch1405
-rw-r--r--gnu/packages/patches/ergodox-firmware-fix-numpad.patch18
4 files changed, 1501 insertions, 1 deletions
diff --git a/gnu/local.mk b/gnu/local.mk
index 8a6e23cacf..9fa52833cb 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -1125,6 +1125,8 @@ dist_patch_DATA = \
%D%/packages/patches/enblend-enfuse-reproducible.patch \
%D%/packages/patches/enjarify-setup-py.patch \
%D%/packages/patches/enlightenment-fix-setuid-path.patch \
+ %D%/packages/patches/ergodox-firmware-fix-json-target.patch \
+ %D%/packages/patches/ergodox-firmware-fix-numpad.patch \
%D%/packages/patches/erlang-man-path.patch \
%D%/packages/patches/esmini-no-clutter-log.patch \
%D%/packages/patches/esmini-use-pkgconfig.patch \
diff --git a/gnu/packages/firmware.scm b/gnu/packages/firmware.scm
index dd982f6ac3..cac67829ed 100644
--- a/gnu/packages/firmware.scm
+++ b/gnu/packages/firmware.scm
@@ -85,7 +85,8 @@
#:use-module (ice-9 format)
#:use-module (ice-9 match)
- #:export (make-qmk-firmware))
+ #:export (make-ergodox-firmware
+ make-qmk-firmware))
(define-public ath9k-htc-firmware
(package
@@ -1224,6 +1225,80 @@ AR100.")
;;;
+;;; ErgoDox firmware.
+;;;
+
+(define* (make-ergodox-firmware/implementation layout #:key override.c
+ override.h)
+ "Return an ergodox-firmware package for LAYOUT, optionally using OVERRIDE.C,
+a C source file-like object to override LAYOUT which may be accompanied by
+OVERRIDE.H, to also override the corresponding layout include file."
+ (let ((revision "0")
+ (commit "89b7e2bfdafb2a87e0248846d5c95cc5e9a27858"))
+ (package
+ (name (string-append "ergodox-firmware-" layout))
+ (version (git-version "1" revision commit))
+ (source (origin
+ (method git-fetch)
+ (uri (git-reference
+ (url "https://github.com/benblazak/ergodox-firmware")
+ (commit commit)))
+ (file-name (git-file-name name version))
+ (sha256
+ (base32
+ "1z28frxyb21nz90frycrpsbxjp09374wawayvjphnwc8njlvkkpy"))
+ (patches
+ (search-patches "ergodox-firmware-fix-json-target.patch"
+ "ergodox-firmware-fix-numpad.patch"))))
+ (build-system gnu-build-system)
+ (arguments
+ (list
+ #:tests? #f ;no test suite
+ #:make-flags
+ #~(list (string-append "LAYOUT=" #$layout)
+ ;; Simplify the output directory name.
+ "ROOT=output")
+ #:phases
+ #~(modify-phases %standard-phases
+ (add-after 'unpack 'copy-override-files
+ (lambda _
+ (when #$override.c
+ (copy-file #$override.c
+ (format #f "src/keyboard/ergodox/layout/~a.c"
+ #$layout)))
+ (when #$override.h
+ (copy-file #$override.h
+ (format #f "src/keyboard/ergodox/layout/~a.h"
+ #$layout)))))
+ ;; The Makefile-based build system lacks configure
+ ;; and install targets.
+ (delete 'configure)
+ (replace 'install
+ (lambda _
+ (with-directory-excursion "output"
+ (install-file "firmware.hex" #$output)
+ (install-file "firmware.eep" #$output)
+ (install-file "firmware--layout.html" #$output)))))))
+ (native-inputs (list (make-avr-toolchain) python))
+ (home-page "https://www.ergodox.io")
+ (synopsis "Firmware for the ErgoDox keyboard")
+ (description (format #f "This package contains the original firmware for
+the ErgoDox keyboard, built using the ~a layout (as defined in the
+@file{src/keyboard/ergodox/layout/~@*~a.c} source file). It contains the
+@file{firmware.hex} and the @file{firmware.eep} files, which can be loaded to
+a target using the @code{teensy-loader-cli} package as well as a
+@file{firmware--layout.html} file, useful to easily visualize the
+corresponding layout." layout))
+ (license license:expat))))
+
+(define make-ergodox-firmware
+ (memoize make-ergodox-firmware/implementation))
+
+(define-public ergodox-firmware-colemak-jc-mod
+ (make-ergodox-firmware "colemak-jc-mod"))
+
+
+;;;
;;; QMK Firmware.
;;;
diff --git a/gnu/packages/patches/ergodox-firmware-fix-json-target.patch b/gnu/packages/patches/ergodox-firmware-fix-json-target.patch
new file mode 100644
index 0000000000..52da4e2497
--- /dev/null
+++ b/gnu/packages/patches/ergodox-firmware-fix-json-target.patch
@@ -0,0 +1,1405 @@
+Submitted upstream:
+<https://github.com/benblazak/ergodox-firmware/pull/99>
+<https://github.com/benblazak/ergodox-firmware/pull/98>
+
+diff --git a/build-scripts/gen-layout.py b/build-scripts/gen-layout.py
+index fd5e54c..251a463 100755
+--- a/build-scripts/gen-layout.py
++++ b/build-scripts/gen-layout.py
+@@ -22,8 +22,10 @@ import sys
+
+ # -----------------------------------------------------------------------------
+
+-class Namespace():
+- pass
++
++class Namespace:
++ pass
++
+
+ template = Namespace()
+ doc = Namespace()
+@@ -31,45 +33,45 @@ info = Namespace()
+
+ # -----------------------------------------------------------------------------
+
++
+ def main():
+- arg_parser = argparse.ArgumentParser(
+- description = "Generate a picture of the firmware's "
+- + "keyboard layout" )
++ arg_parser = argparse.ArgumentParser(
++ description="Generate a picture of the firmware's " + "keyboard layout"
++ )
+
+- arg_parser.add_argument(
+- '--ui-info-file',
+- required = True )
++ arg_parser.add_argument("--ui-info-file", required=True)
+
+- args = arg_parser.parse_args(sys.argv[1:])
++ args = arg_parser.parse_args(sys.argv[1:])
+
+- # constant file paths
+- args.template_svg_file = './build-scripts/gen_layout/template.svg'
+- args.template_js_file = './build-scripts/gen_layout/template.js'
++ # constant file paths
++ args.template_svg_file = "./build-scripts/gen_layout/template.svg"
++ args.template_js_file = "./build-scripts/gen_layout/template.js"
+
+- # normalize paths
+- args.ui_info_file = os.path.abspath(args.ui_info_file)
+- args.template_svg_file = os.path.abspath(args.template_svg_file)
+- args.template_js_file = os.path.abspath(args.template_js_file)
++ # normalize paths
++ args.ui_info_file = os.path.abspath(args.ui_info_file)
++ args.template_svg_file = os.path.abspath(args.template_svg_file)
++ args.template_js_file = os.path.abspath(args.template_js_file)
+
+- # set vars
+- doc.main = '' # to store the html document we're generating
+- template.svg = open(args.template_svg_file).read()
+- template.js = open(args.template_js_file).read()
+- info.all = json.loads(open(args.ui_info_file).read())
++ # set vars
++ doc.main = "" # to store the html document we're generating
++ template.svg = open(args.template_svg_file).read()
++ template.js = open(args.template_js_file).read()
++ info.all = json.loads(open(args.ui_info_file).read())
+
+- info.matrix_positions = info.all['mappings']['matrix-positions']
+- info.matrix_layout = info.all['mappings']['matrix-layout']
++ info.matrix_positions = info.all["mappings"]["matrix-positions"]
++ info.matrix_layout = info.all["mappings"]["matrix-layout"]
+
+- # prefix
+- doc.prefix = ("""
++ # prefix
++ doc.prefix = (
++ """
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
+ <html>
+
+ <head>
+ <script>
+ """
+-+ template.js +
+-""" </script>
++ + template.js
++ + """ </script>
+ </head>
+
+ <body>
+@@ -78,9 +80,13 @@ def main():
+
+ <ul>
+ <li>git commit date:
+- <code>""" + info.all['miscellaneous']['git-commit-date'] + """</code></li>
++ <code>"""
++ + info.all["miscellaneous"]["git-commit-date"]
++ + """</code></li>
+ <li>git commit id:
+- <code>""" + info.all['miscellaneous']['git-commit-id'] + """</code></li>
++ <code>"""
++ + info.all["miscellaneous"]["git-commit-id"]
++ + """</code></li>
+ </ul>
+
+ <h2>Notes</h2>
+@@ -123,301 +129,293 @@ def main():
+
+ <br>
+
+-""")[1:-1]
++"""
++ )[1:-1]
+
+- # suffix
+- doc.suffix = ("""
++ # suffix
++ doc.suffix = (
++ """
+ </body>
+ </html>
+
+-""")[1:-1]
+-
+- # substitute into template
+- # -------
+- # note: this is not general enough to handle any possible layout well, at
+- # the moment. but it should handle more standard ones well. (hopefully
+- # minor) modifications may be necessary on a case by case basis
+- # -------
+- layer_number = -1
+- for (layout, layer) in zip( info.matrix_layout,
+- range(len(info.matrix_layout))):
+- layer_number += 1
+- svg = template.svg
+- for (name, (code, press, release)) \
+- in zip(info.matrix_positions, layout):
+- replace = ''
+- if press == 'kbfun_transparent':
+- replace = ''
+- elif press == 'kbfun_shift_press_release':
+- replace = 'sh ' + keycode_to_string.get(code, '[n/a]')
+- elif press == 'kbfun_jump_to_bootloader':
+- replace = '[btldr]'
+- elif press == 'NULL' and release == 'NULL':
+- replace = '(null)'
+- elif re.search(r'numpad', press+release):
+- replace = '[num]'
+- elif re.search(r'layer', press+release):
+- replace = 'la ' + re.findall(r'\d+', press+release)[0] + ' '
+- if re.search(r'push', press+release):
+- replace += '+'
+- if re.search(r'pop', press+release):
+- replace += '-'
+- replace += ' ' + str(code)
+- else:
+- replace = keycode_to_string.get(code, '[n/a]')
+-
+- svg = re.sub(
+- '>'+name+'<', '>'+replace+'<', svg )
+- svg = re.sub(
+- r"\('(" + name + r".*)'\)",
+- r"('\1', " + str(layer) + r")",
+- svg )
+-
+- doc.main += '<h2>Layer ' + str(layer_number) + '</h2>\n' + svg
+-
+- # change the font size
+- doc.main = re.sub(r'22.5px', '15px', doc.main)
+-
+- print(doc.prefix + doc.main + doc.suffix)
++"""
++ )[1:-1]
++
++ # substitute into template
++ # -------
++ # note: this is not general enough to handle any possible layout well, at
++ # the moment. but it should handle more standard ones well. (hopefully
++ # minor) modifications may be necessary on a case by case basis
++ # -------
++ layer_number = -1
++ for (layout, layer) in zip(
++ info.matrix_layout, range(len(info.matrix_layout))
++ ):
++ layer_number += 1
++ svg = template.svg
++ for (name, (code, press, release)) in zip(
++ info.matrix_positions, layout
++ ):
++ replace = ""
++ if press == "kbfun_transparent":
++ replace = ""
++ elif press == "kbfun_shift_press_release":
++ replace = "sh " + keycode_to_string.get(code, "[n/a]")
++ elif press == "kbfun_jump_to_bootloader":
++ replace = "[btldr]"
++ elif press == "NULL" and release == "NULL":
++ replace = "(null)"
++ elif re.search(r"numpad", press + release):
++ replace = "[num]"
++ elif re.search(r"layer", press + release):
++ replace = "la " + re.findall(r"\d+", press + release)[0] + " "
++ if re.search(r"push", press + release):
++ replace += "+"
++ if re.search(r"pop", press + release):
++ replace += "-"
++ replace += " " + str(code)
++ else:
++ replace = keycode_to_string.get(code, "[n/a]")
++
++ svg = re.sub(">" + name + "<", ">" + replace + "<", svg)
++ svg = re.sub(
++ r"\('(" + name + r".*)'\)", r"('\1', " + str(layer) + r")", svg
++ )
++
++ doc.main += "<h2>Layer " + str(layer_number) + "</h2>\n" + svg
++
++ # change the font size
++ doc.main = re.sub(r"22.5px", "15px", doc.main)
++
++ print(doc.prefix + doc.main + doc.suffix)
++
+
+ # -----------------------------------------------------------------------------
+ # -----------------------------------------------------------------------------
+
+ keycode_to_string = {
+- 0x01: "Error", # ErrorRollOver
+- 0x02: "POSTFail",
+- 0x03: "Error", # ErrorUndefined
+- 0x04: "a A",
+- 0x05: "b B",
+- 0x06: "c C",
+- 0x07: "d D",
+- 0x08: "e E",
+- 0x09: "f F",
+- 0x0A: "g G",
+- 0x0B: "h H",
+- 0x0C: "i I",
+- 0x0D: "j J",
+- 0x0E: "k K",
+- 0x0F: "l L",
+- 0x10: "m M",
+- 0x11: "n N",
+- 0x12: "o O",
+- 0x13: "p P",
+- 0x14: "q Q",
+- 0x15: "r R",
+- 0x16: "s S",
+- 0x17: "t T",
+- 0x18: "u U",
+- 0x19: "v V",
+- 0x1A: "w W",
+- 0x1B: "x X",
+- 0x1C: "y Y",
+- 0x1D: "z Z",
+- 0x1E: "1 !",
+- 0x1F: "2 @",
+- 0x20: "3 #",
+- 0x21: "4 $",
+- 0x22: "5 %",
+- 0x23: "6 ^",
+- 0x24: "7 &",
+- 0x25: "8 *",
+- 0x26: "9 (",
+- 0x27: "0 )",
+- 0x28: "Return",
+- 0x29: "Esc",
+- 0x2A: "Backspace",
+- 0x2B: "Tab",
+- 0x2C: "Space",
+- 0x2D: "- _",
+- 0x2E: "= +",
+- 0x2F: "[ {",
+- 0x30: "] }",
+- 0x31: "\ |",
+- 0x32: "# ~",
+- 0x33: "; :",
+- 0x34: "\' \"",
+- 0x35: "` ~",
+- 0x36: ", <",
+- 0x37: ". >",
+- 0x38: "/ ?",
+- 0x39: "Caps",
+- 0x3A: "F1",
+- 0x3B: "F2",
+- 0x3C: "F3",
+- 0x3D: "F4",
+- 0x3E: "F5",
+- 0x3F: "F6",
+- 0x40: "F7",
+- 0x41: "F8",
+- 0x42: "F9",
+- 0x43: "F10",
+- 0x44: "F11",
+- 0x45: "F12",
+- 0x46: "PrintScreen",
+- 0x47: "ScrollLock",
+- 0x48: "Pause",
+- 0x49: "Ins", # Insert
+- 0x4A: "Hm", # Home
+- 0x4B: "Pg\u2191", # up arrow
+- 0x4C: "Delete",
+- 0x4D: "End",
+- 0x4E: "Pg\u2193", # down arrow
+- 0x4F: "\u2192", # right arrow
+- 0x50: "\u2190", # left arrow
+- 0x51: "\u2193", # down arrow
+- 0x52: "\u2191", # up arrow
+-
+- 0x53: "Num",
+- 0x54: "/",
+- 0x55: "*",
+- 0x56: "-",
+- 0x57: "+",
+- 0x58: "Enter",
+- 0x59: "1 End",
+- 0x5A: "2 \u2193", # down arrow
+- 0x5B: "3 Pg\u2193", # down arrow
+- 0x5C: "4 \u2190", # left arrow
+- 0x5D: "5",
+- 0x5E: "6 \u2192", # right arrow
+- 0x5F: "7 Hm", # Home
+- 0x60: "8 \u2191", # up arrow
+- 0x61: "9 Pg\u2191", # up arrow
+- 0x62: "0 Ins", # Insert
+- 0x63: ". Del",
+-
+- 0x64: "\ |",
+- 0x65: "App",
+- 0x66: "Power",
+-
+- 0x67: "=",
+-
+- 0x68: "F13",
+- 0x69: "F14",
+- 0x6A: "F15",
+- 0x6B: "F16",
+- 0x6C: "F17",
+- 0x6D: "F18",
+- 0x6E: "F19",
+- 0x6F: "F20",
+- 0x70: "F21",
+- 0x71: "F22",
+- 0x72: "F23",
+- 0x73: "F24",
+- 0x74: "Exec",
+- 0x75: "Help",
+- 0x76: "Menu",
+- 0x77: "Select",
+- 0x78: "Stop",
+- 0x79: "Again",
+- 0x7A: "Undo",
+- 0x7B: "Cut",
+- 0x7C: "Copy",
+- 0x7D: "Paste",
+- 0x7E: "Find",
+- 0x7F: "Mute",
+- 0x80: "VolUp",
+- 0x81: "VolDown",
+- 0x82: "LockingCapsLock",
+- 0x83: "LockingNumLock",
+- 0x84: "LockingScrollLock",
+-
+- 0x85: ",",
+- 0x86: "=",
+-
+- 0x87: "Int1",
+- 0x88: "Int2",
+- 0x89: "Int3",
+- 0x8A: "Int4",
+- 0x8B: "Int5",
+- 0x8C: "Int6",
+- 0x8D: "Int7",
+- 0x8E: "Int8",
+- 0x8F: "Int9",
+- 0x90: "LANG1",
+- 0x91: "LANG2",
+- 0x92: "LANG3",
+- 0x93: "LANG4",
+- 0x94: "LANG5",
+- 0x95: "LANG6",
+- 0x96: "LANG7",
+- 0x97: "LANG8",
+- 0x98: "LANG9",
+- 0x99: "AlternateErase",
+- 0x9A: "SysReq_Attention",
+- 0x9B: "Cancel",
+- 0x9C: "Clear",
+- 0x9D: "Prior",
+- 0x9E: "Return",
+- 0x9F: "Separator",
+- 0xA0: "Out",
+- 0xA1: "Oper",
+- 0xA2: "Clear_Again",
+- 0xA3: "CrSel_Props",
+- 0xA4: "ExSel",
+-
+- 0xB0: "00",
+- 0xB1: "000",
+-
+- 0xB2: "Thousands_Sep",
+- 0xB3: "Decimal_Sep",
+- 0xB4: "$",
+- 0xB5: "Currency_Subunit",
+-
+- 0xB6: "(",
+- 0xB7: ")",
+- 0xB8: "{",
+- 0xB9: "}",
+-
+- 0xBA: "Tab",
+- 0xBB: "Backspace",
+- 0xBC: "A",
+- 0xBD: "B",
+- 0xBE: "C",
+- 0xBF: "D",
+- 0xC0: "E",
+- 0xC1: "F",
+- 0xC2: "XOR",
+- 0xC3: "^",
+- 0xC4: "%",
+- 0xC5: "<",
+- 0xC6: ">",
+- 0xC7: "&",
+- 0xC8: "&&",
+- 0xC9: "|",
+- 0xCA: "||",
+- 0xCB: ":",
+- 0xCC: "#",
+- 0xCD: "Space",
+- 0xCE: "@",
+- 0xCF: "!",
+- 0xD0: "Mem_Store",
+- 0xD1: "Mem_Recall",
+- 0xD2: "Mem_Clear",
+- 0xD3: "Mem_+",
+- 0xD4: "Mem_-",
+- 0xD5: "Mem_*",
+- 0xD6: "Mem_/",
+- 0xD7: "+-",
+- 0xD8: "Clear",
+- 0xD9: "ClearEntry",
+- 0xDA: "Binary",
+- 0xDB: "Octal",
+- 0xDC: ".",
+- 0xDD: "Hexadecimal",
+-
+- 0xE0: "L-Ctrl",
+- 0xE1: "L-Shift",
+- 0xE2: "L-Alt",
+- 0xE3: "L-GUI",
+- 0xE4: "R-Ctrl",
+- 0xE5: "R-Shift",
+- 0xE6: "R-Alt",
+- 0xE7: "R-GUI",
+- }
++ 0x01: "Error", # ErrorRollOver
++ 0x02: "POSTFail",
++ 0x03: "Error", # ErrorUndefined
++ 0x04: "a A",
++ 0x05: "b B",
++ 0x06: "c C",
++ 0x07: "d D",
++ 0x08: "e E",
++ 0x09: "f F",
++ 0x0A: "g G",
++ 0x0B: "h H",
++ 0x0C: "i I",
++ 0x0D: "j J",
++ 0x0E: "k K",
++ 0x0F: "l L",
++ 0x10: "m M",
++ 0x11: "n N",
++ 0x12: "o O",
++ 0x13: "p P",
++ 0x14: "q Q",
++ 0x15: "r R",
++ 0x16: "s S",
++ 0x17: "t T",
++ 0x18: "u U",
++ 0x19: "v V",
++ 0x1A: "w W",
++ 0x1B: "x X",
++ 0x1C: "y Y",
++ 0x1D: "z Z",
++ 0x1E: "1 !",
++ 0x1F: "2 @",
++ 0x20: "3 #",
++ 0x21: "4 $",
++ 0x22: "5 %",
++ 0x23: "6 ^",
++ 0x24: "7 &",
++ 0x25: "8 *",
++ 0x26: "9 (",
++ 0x27: "0 )",
++ 0x28: "Return",
++ 0x29: "Esc",
++ 0x2A: "Backspace",
++ 0x2B: "Tab",
++ 0x2C: "Space",
++ 0x2D: "- _",
++ 0x2E: "= +",
++ 0x2F: "[ {",
++ 0x30: "] }",
++ 0x31: "\ |",
++ 0x32: "# ~",
++ 0x33: "; :",
++ 0x34: "' \"",
++ 0x35: "` ~",
++ 0x36: ", <",
++ 0x37: ". >",
++ 0x38: "/ ?",
++ 0x39: "Caps",
++ 0x3A: "F1",
++ 0x3B: "F2",
++ 0x3C: "F3",
++ 0x3D: "F4",
++ 0x3E: "F5",
++ 0x3F: "F6",
++ 0x40: "F7",
++ 0x41: "F8",
++ 0x42: "F9",
++ 0x43: "F10",
++ 0x44: "F11",
++ 0x45: "F12",
++ 0x46: "PrintScreen",
++ 0x47: "ScrollLock",
++ 0x48: "Pause",
++ 0x49: "Ins", # Insert
++ 0x4A: "Hm", # Home
++ 0x4B: "Pg\u2191", # up arrow
++ 0x4C: "Delete",
++ 0x4D: "End",
++ 0x4E: "Pg\u2193", # down arrow
++ 0x4F: "\u2192", # right arrow
++ 0x50: "\u2190", # left arrow
++ 0x51: "\u2193", # down arrow
++ 0x52: "\u2191", # up arrow
++ 0x53: "Num",
++ 0x54: "/",
++ 0x55: "*",
++ 0x56: "-",
++ 0x57: "+",
++ 0x58: "Enter",
++ 0x59: "1 End",
++ 0x5A: "2 \u2193", # down arrow
++ 0x5B: "3 Pg\u2193", # down arrow
++ 0x5C: "4 \u2190", # left arrow
++ 0x5D: "5",
++ 0x5E: "6 \u2192", # right arrow
++ 0x5F: "7 Hm", # Home
++ 0x60: "8 \u2191", # up arrow
++ 0x61: "9 Pg\u2191", # up arrow
++ 0x62: "0 Ins", # Insert
++ 0x63: ". Del",
++ 0x64: "\ |",
++ 0x65: "App",
++ 0x66: "Power",
++ 0x67: "=",
++ 0x68: "F13",
++ 0x69: "F14",
++ 0x6A: "F15",
++ 0x6B: "F16",
++ 0x6C: "F17",
++ 0x6D: "F18",
++ 0x6E: "F19",
++ 0x6F: "F20",
++ 0x70: "F21",
++ 0x71: "F22",
++ 0x72: "F23",
++ 0x73: "F24",
++ 0x74: "Exec",
++ 0x75: "Help",
++ 0x76: "Menu",
++ 0x77: "Select",
++ 0x78: "Stop",
++ 0x79: "Again",
++ 0x7A: "Undo",
++ 0x7B: "Cut",
++ 0x7C: "Copy",
++ 0x7D: "Paste",
++ 0x7E: "Find",
++ 0x7F: "Mute",
++ 0x80: "VolUp",
++ 0x81: "VolDown",
++ 0x82: "LockingCapsLock",
++ 0x83: "LockingNumLock",
++ 0x84: "LockingScrollLock",
++ 0x85: ",",
++ 0x86: "=",
++ 0x87: "Int1",
++ 0x88: "Int2",
++ 0x89: "Int3",
++ 0x8A: "Int4",
++ 0x8B: "Int5",
++ 0x8C: "Int6",
++ 0x8D: "Int7",
++ 0x8E: "Int8",
++ 0x8F: "Int9",
++ 0x90: "LANG1",
++ 0x91: "LANG2",
++ 0x92: "LANG3",
++ 0x93: "LANG4",
++ 0x94: "LANG5",
++ 0x95: "LANG6",
++ 0x96: "LANG7",
++ 0x97: "LANG8",
++ 0x98: "LANG9",
++ 0x99: "AlternateErase",
++ 0x9A: "SysReq_Attention",
++ 0x9B: "Cancel",
++ 0x9C: "Clear",
++ 0x9D: "Prior",
++ 0x9E: "Return",
++ 0x9F: "Separator",
++ 0xA0: "Out",
++ 0xA1: "Oper",
++ 0xA2: "Clear_Again",
++ 0xA3: "CrSel_Props",
++ 0xA4: "ExSel",
++ 0xB0: "00",
++ 0xB1: "000",
++ 0xB2: "Thousands_Sep",
++ 0xB3: "Decimal_Sep",
++ 0xB4: "$",
++ 0xB5: "Currency_Subunit",
++ 0xB6: "(",
++ 0xB7: ")",
++ 0xB8: "{",
++ 0xB9: "}",
++ 0xBA: "Tab",
++ 0xBB: "Backspace",
++ 0xBC: "A",
++ 0xBD: "B",
++ 0xBE: "C",
++ 0xBF: "D",
++ 0xC0: "E",
++ 0xC1: "F",
++ 0xC2: "XOR",
++ 0xC3: "^",
++ 0xC4: "%",
++ 0xC5: "<",
++ 0xC6: ">",
++ 0xC7: "&",
++ 0xC8: "&&",
++ 0xC9: "|",
++ 0xCA: "||",
++ 0xCB: ":",
++ 0xCC: "#",
++ 0xCD: "Space",
++ 0xCE: "@",
++ 0xCF: "!",
++ 0xD0: "Mem_Store",
++ 0xD1: "Mem_Recall",
++ 0xD2: "Mem_Clear",
++ 0xD3: "Mem_+",
++ 0xD4: "Mem_-",
++ 0xD5: "Mem_*",
++ 0xD6: "Mem_/",
++ 0xD7: "+-",
++ 0xD8: "Clear",
++ 0xD9: "ClearEntry",
++ 0xDA: "Binary",
++ 0xDB: "Octal",
++ 0xDC: ".",
++ 0xDD: "Hexadecimal",
++ 0xE0: "L-Ctrl",
++ 0xE1: "L-Shift",
++ 0xE2: "L-Alt",
++ 0xE3: "L-GUI",
++ 0xE4: "R-Ctrl",
++ 0xE5: "R-Shift",
++ 0xE6: "R-Alt",
++ 0xE7: "R-GUI",
++}
+
+ # -----------------------------------------------------------------------------
+ # -----------------------------------------------------------------------------
+
+-if __name__ == '__main__':
+- main()
+-
++if __name__ == "__main__":
++ main()
+diff --git a/build-scripts/gen-ui-info.py b/build-scripts/gen-ui-info.py
+index 1c93d32..0fa52e3 100755
+--- a/build-scripts/gen-ui-info.py
++++ b/build-scripts/gen-ui-info.py
+@@ -13,7 +13,16 @@ Depends on:
+ - the project '.map' file (generated by the compiler)
+ """
+
+-_FORMAT_DESCRIPTION = ("""
++import argparse
++import json
++import os
++import pathlib
++import re
++import subprocess
++import sys
++
++_FORMAT_DESCRIPTION = (
++ """
+ /* ----------------------------------------------------------------------------
+ * Version 0
+ * ----------------------------------------------------------------------------
+@@ -31,7 +40,7 @@ var ui_info = {
+ ".meta-data": { // for the JSON file
+ "version": "<number>",
+ "date-generated": "<string>", // format: RFC 3339
+- "description": "<string>",
++ "description": "<string>",
+ },
+ "keyboard-functions": {
+ "<(function name)>": {
+@@ -57,7 +66,7 @@ var ui_info = {
+ "..."
+ },
+ "mappings": {
+- /*
++ /*
+ * The mappings prefixed with 'matrix' have their elements in the same
+ * order as the .hex file (whatever order that is). The mappings
+ * prefixed with 'physical' will have their elements in an order
+@@ -113,365 +122,304 @@ var ui_info = {
+ "number-of-layers": "<number>"
+ }
+ }
+-""")[1:-1]
++"""
++)[1:-1]
+
+ # -----------------------------------------------------------------------------
+
+-import argparse
+-import json
+-import os
+-import re
+-import subprocess
+-import sys
+-
+-# -----------------------------------------------------------------------------
+
+ def gen_static(current_date=None, git_commit_date=None, git_commit_id=None):
+- """Generate static information"""
+-
+- return {
+- '.meta-data': {
+- 'version': 0, # the format version number
+- 'date-generated': current_date,
+- 'description': _FORMAT_DESCRIPTION,
+- },
+- 'miscellaneous': {
+- 'git-commit-date': git_commit_date, # should be passed by makefile
+- 'git-commit-id': git_commit_id, # should be passed by makefile
+- },
+- }
+-
+-def gen_derived(data):
+- return {} # don't really need this info anymore
+-# """
+-# Generate derived information
+-# Should be called last
+-# """
+-# return {
+-# 'miscellaneous': {
+-# 'number-of-layers':
+-# int( data['layout-matrices']['_kb_layout']['length']/(6*14) ),
+-# # because 6*14 is the number of bytes/layer for '_kb_layout'
+-# # (which is a uint8_t matrix)
+-# },
+-# }
+-
+-# -----------------------------------------------------------------------------
++ """Generate static information"""
+
+-def parse_mapfile(map_file_path):
+- return {} # don't really need this info anymore
+-# """Parse the '.map' file"""
+-#
+-# def parse_keyboard_function(f, line):
+-# """Parse keyboard-functions in the '.map' file"""
+-#
+-# search = re.search(r'(0x\S+)\s+(0x\S+)', next(f))
+-# position = int( search.group(1), 16 )
+-# length = int( search.group(2), 16 )
+-#
+-# search = re.search(r'0x\S+\s+(\S+)', next(f))
+-# name = search.group(1)
+-#
+-# return {
+-# 'keyboard-functions': {
+-# name: {
+-# 'position': position,
+-# 'length': length,
+-# },
+-# },
+-# }
+-#
+-# def parse_layout_matrices(f, line):
+-# """Parse layout matrix information in the '.map' file"""
+-#
+-# name = re.search(r'.progmem.data.(_kb_layout\S*)', line).group(1)
+-#
+-# search = re.search(r'(0x\S+)\s+(0x\S+)', next(f))
+-# position = int( search.group(1), 16 )
+-# length = int( search.group(2), 16 )
+-#
+-# return {
+-# 'layout-matrices': {
+-# name: {
+-# 'position': position,
+-# 'length': length,
+-# },
+-# },
+-# }
+-#
+-# # --- parse_mapfile() ---
+-#
+-# # normalize paths
+-# map_file_path = os.path.abspath(map_file_path)
+-# # check paths
+-# if not os.path.exists(map_file_path):
+-# raise ValueError("invalid 'map_file_path' given")
+-#
+-# output = {}
+-#
+-# f = open(map_file_path)
+-#
+-# for line in f:
+-# if re.search(r'^\s*\.text\.kbfun_', line):
+-# dict_merge(output, parse_keyboard_function(f, line))
+-# elif re.search(r'^\s*\.progmem\.data.*layout', line):
+-# dict_merge(output, parse_layout_matrices(f, line))
+-#
+-# return output
++ return {
++ ".meta-data": {
++ "version": 0, # the format version number
++ "date-generated": current_date,
++ "description": _FORMAT_DESCRIPTION,
++ },
++ "miscellaneous": {
++ "git-commit-date": git_commit_date, # should be passed by makefile
++ "git-commit-id": git_commit_id, # should be passed by makefile
++ },
++ }
+
+
+ def find_keyboard_functions(source_code_path):
+- """Parse all files in the source directory"""
+-
+- def read_comments(f, line):
+- """
+- Read in properly formatted multi-line comments
+- - Comments must start with '/*' and end with '*/', each on their own
+- line
+- """
+- comments = ''
+- while(line.strip() != r'*/'):
+- comments += line[2:].strip()+'\n'
+- line = next(f)
+- return comments
+-
+- def parse_comments(comments):
+- """
+- Parse an INI style comment string
+- - Fields begin with '[field-name]', and continue until the next field,
+- or the end of the comment
+- - Fields '[name]', '[description]', and '[note]' are treated specially
+- """
+-
+- def add_field(output, field, value):
+- """Put a field+value pair in 'output', the way we want it, if the
+- pair is valid"""
+-
+- value = value.strip()
+-
+- if field is not None:
+- if field in ('name', 'description'):
+- if field not in output:
+- output[field] = value
+- else:
+- if field == 'note':
+- field = 'notes'
+-
+- if field not in output:
+- output[field] = []
+-
+- output[field] += [value]
+-
+- # --- parse_comments() ---
+-
+- output = {}
+-
+- field = None
+- value = None
+- for line in comments.split('\n'):
+- line = line.strip()
+-
+- if re.search(r'^\[.*\]$', line):
+- add_field(output, field, value)
+- field = line[1:-1]
+- value = None
+-
+- else:
+- if value is None:
+- value = ''
+- if len(value) > 0 and value[-1] == '.':
+- line = ' '+line
+- value += ' '+line
+-
+- add_field(output, field, value)
+-
+- return output
+-
+- def parse_keyboard_function(f, line, comments):
+- """Parse keyboard-functions in the source code"""
+-
+- search = re.search(r'void\s+(kbfun_\S+)\s*\(void\)', line)
+- name = search.group(1)
+-
+- return {
+- 'keyboard-functions': {
+- name: {
+- 'comments': parse_comments(comments),
+- },
+- },
+- }
+-
+- # --- find_keyboard_functions() ---
+-
+- # normalize paths
+- source_code_path = os.path.abspath(source_code_path)
+- # check paths
+- if not os.path.exists(source_code_path):
+- raise ValueError("invalid 'source_code_path' given")
+-
+- output = {}
+-
+- for tup in os.walk(source_code_path):
+- for file_name in tup[2]:
+- # normalize paths
+- file_name = os.path.abspath( os.path.join( tup[0], file_name ) )
+-
+- # ignore non '.c' files
+- if file_name[-2:] != '.c':
+- continue
+-
+- f = open(file_name)
+-
+- comments = ''
+- for line in f:
+- if line.strip() == r'/*':
+- comments = read_comments(f, line)
+- elif re.search(r'void\s+kbfun_\S+\s*\(void\)', line):
+- dict_merge(
+- output,
+- parse_keyboard_function(f, line, comments) )
+-
+- return output
++ """Parse all files in the source directory"""
++
++ def read_comments(f, line):
++ """
++ Read in properly formatted multi-line comments
++ - Comments must start with '/*' and end with '*/', each on their own
++ line
++ """
++ comments = ""
++ while line.strip() != r"*/":
++ comments += line[2:].strip() + "\n"
++ line = next(f)
++ return comments
++
++ def parse_comments(comments):
++ """
++ Parse an INI style comment string
++ - Fields begin with '[field-name]', and continue until the next field,
++ or the end of the comment
++ - Fields '[name]', '[description]', and '[note]' are treated specially
++ """
++
++ def add_field(output, field, value):
++ """Put a field+value pair in 'output', the way we want it, if the
++ pair is valid"""
++
++ value = value.strip()
++
++ if field is not None:
++ if field in ("name", "description"):
++ if field not in output:
++ output[field] = value
++ else:
++ if field == "note":
++ field = "notes"
++
++ if field not in output:
++ output[field] = []
++
++ output[field] += [value]
++
++ # --- parse_comments() ---
++
++ output = {}
++
++ field = None
++ value = None
++ for line in comments.split("\n"):
++ line = line.strip()
++
++ if re.search(r"^\[.*\]$", line):
++ add_field(output, field, value)
++ field = line[1:-1]
++ value = None
++ else:
++ if value is None:
++ value = ""
++ if len(value) > 0 and value[-1] == ".":
++ line = " " + line
++ value += " " + line
++
++ add_field(output, field, value)
++
++ return output
++
++ def parse_keyboard_function(f, line, comments):
++ """Parse keyboard-functions in the source code"""
++
++ search = re.search(r"void\s+(kbfun_\S+)\s*\(void\)", line)
++ name = search.group(1)
++
++ return {
++ "keyboard-functions": {
++ name: {
++ "comments": parse_comments(comments),
++ },
++ },
++ }
++
++ # --- find_keyboard_functions() ---
++
++ # normalize paths
++ source_code_path = os.path.abspath(source_code_path)
++ # check paths
++ if not os.path.exists(source_code_path):
++ raise ValueError("invalid 'source_code_path' given")
++
++ output = {}
++
++ for tup in os.walk(source_code_path):
++ for file_name in tup[2]:
++ # normalize paths
++ file_name = os.path.abspath(os.path.join(tup[0], file_name))
++
++ # ignore non '.c' files
++ if file_name[-2:] != ".c":
++ continue
++
++ f = open(file_name)
++
++ comments = ""
++ for line in f:
++ if line.strip() == r"/*":
++ comments = read_comments(f, line)
++ elif re.search(r"void\s+kbfun_\S+\s*\(void\)", line):
++ dict_merge(
++ output, parse_keyboard_function(f, line, comments)
++ )
++
++ return output
+
+
+ def gen_mappings(matrix_file_path, layout_file_path):
+- # normalize paths
+- matrix_file_path = os.path.abspath(matrix_file_path)
+- layout_file_path = os.path.abspath(layout_file_path)
+-
+- def parse_matrix_file(matrix_file_path):
+- match = re.search( # find the whole 'KB_MATRIX_LAYER' macro
+- r'#define\s+KB_MATRIX_LAYER\s*\(([^)]+)\)[^{]*\{\{([^#]+)\}\}',
+- open(matrix_file_path).read() )
+-
+- return {
+- "mappings": {
+- "physical-positions": re.findall(r'k..', match.group(1)),
+- "matrix-positions": re.findall(r'k..|na', match.group(2)),
+- },
+- }
+-
+- def parse_layout_file(layout_file_path):
+- match = re.findall( # find each whole '_kb_layout*' matrix definition
+- r'(_kb_layout\w*)[^=]*=((?:[^{}]*\{){3}[^=]*(?:[^{}]*\}){3})',
+- subprocess.getoutput("gcc -E '"+layout_file_path+"'") )
+-
+- layout = {}
+- # collect all the values
+- for (name, matrix) in match:
+- layout[name] = [
+- re.findall( # find all numbers and function pointers
+- r'[x0-9A-F]+|&\w+|NULL',
+- re.sub( # replace '((void *) 0)' with 'NULL'
+- r'\(\s*\(\s*void\s*\*\s*\)\s*0\s*\)',
+- 'NULL',
+- el ) )
+- for el in
+- re.findall( # find each whole layer
+- r'(?:[^{}]*\{){2}((?:[^}]|\}\s*,)+)(?:[^{}]*\}){2}',
+- matrix ) ]
+-
+- # make the numbers into actual numbers
+- layout['_kb_layout'] = \
+- [[eval(el) for el in layer] for layer in layout['_kb_layout']]
+- # remove the preceeding '&' from function pointers
+- for matrix in ('_kb_layout_press', '_kb_layout_release'):
+- layout[matrix] = \
+- [ [re.sub(r'&', '', el) for el in layer]
+- for layer in layout[matrix] ]
+-
+- return {
+- "mappings": {
+- "matrix-layout":
+- # group them all properly
+- [ [[c, p, r] for (c, p, r) in zip(code, press, release)]
+- for (code, press, release) in
+- zip( layout['_kb_layout'],
+- layout['_kb_layout_press'],
+- layout['_kb_layout_release'] ) ]
+- },
+- }
+-
+- return dict_merge(
+- parse_matrix_file(matrix_file_path),
+- parse_layout_file(layout_file_path) )
++ # normalize paths
++ matrix_file_path = os.path.abspath(matrix_file_path)
++ layout_file_path = os.path.abspath(layout_file_path)
++ layout_name = pathlib.Path(layout_file_path).with_suffix('').name
++
++ def parse_matrix_file(matrix_file_path):
++ match = re.search( # find the whole 'KB_MATRIX_LAYER' macro
++ r"#define\s+KB_MATRIX_LAYER\s*\(([^)]+)\)[^{]*\{\{([^#]+)\}\}",
++ open(matrix_file_path).read(),
++ )
++
++ return {
++ "mappings": {
++ "physical-positions": re.findall(r"k..", match.group(1)),
++ "matrix-positions": re.findall(r"k..|na", match.group(2)),
++ },
++ }
++
++ def parse_layout_file(layout_file_path):
++ output = subprocess.check_output(
++ ['avr-gcc', f'-DMAKEFILE_KEYBOARD_LAYOUT={layout_name}',
++ '-E', layout_file_path], encoding='UTF-8')
++ match = re.findall( # find each whole '_kb_layout*' matrix definition
++ r"(_kb_layout\w*)[^=]*=((?:[^{}]*\{){3}[^=]*(?:[^{}]*\}){3})",
++ output,
++ )
++
++ layout = {}
++ # collect all the values
++ for (name, matrix) in match:
++ layout[name] = [
++ re.findall( # find all numbers and function pointers
++ r"[x0-9A-F]+|&\w+|NULL",
++ re.sub( # replace '((void *) 0)' with 'NULL'
++ r"\(\s*\(\s*void\s*\*\s*\)\s*0\s*\)", "NULL", el
++ ),
++ )
++ for el in re.findall( # find each whole layer
++ r"(?:[^{}]*\{){2}((?:[^}]|\}\s*,)+)(?:[^{}]*\}){2}", matrix
++ )
++ ]
++
++ # make the numbers into actual numbers
++ layout["_kb_layout"] = [
++ [eval(el) for el in layer] for layer in layout["_kb_layout"]
++ ]
++ # remove the preceeding '&' from function pointers
++ for matrix in ("_kb_layout_press", "_kb_layout_release"):
++ layout[matrix] = [
++ [re.sub(r"&", "", el) for el in layer]
++ for layer in layout[matrix]
++ ]
++
++ return {
++ "mappings": {
++ "matrix-layout":
++ # group them all properly
++ [
++ [[c, p, r] for (c, p, r) in zip(code, press, release)]
++ for (code, press, release) in zip(
++ layout["_kb_layout"],
++ layout["_kb_layout_press"],
++ layout["_kb_layout_release"],
++ )
++ ]
++ },
++ }
++
++ return dict_merge(
++ parse_matrix_file(matrix_file_path),
++ parse_layout_file(layout_file_path),
++ )
+
+
+ # -----------------------------------------------------------------------------
+
++
+ def dict_merge(a, b):
+- """
+- Recursively merge two dictionaries
+- - I was looking around for an easy way to do this, and found something
+- [here]
+- (http://www.xormedia.com/recursively-merge-dictionaries-in-python.html).
+- This is pretty close, but i didn't copy it exactly.
+- """
++ """
++ Recursively merge two dictionaries
++ - I was looking around for an easy way to do this, and found something
++ [here]
++ (http://www.xormedia.com/recursively-merge-dictionaries-in-python.html).
++ This is pretty close, but i didn't copy it exactly.
++ """
++
++ if not isinstance(a, dict) or not isinstance(b, dict):
++ return b
+
+- if not isinstance(a, dict) or not isinstance(b, dict):
+- return b
++ for (key, value) in b.items():
++ if key in a:
++ a[key] = dict_merge(a[key], value)
++ else:
++ a[key] = value
+
+- for (key, value) in b.items():
+- if key in a:
+- a[key] = dict_merge(a[key], value)
+- else:
+- a[key] = value
++ return a
+
+- return a
+
+ # -----------------------------------------------------------------------------
+
++
+ def main():
+- arg_parser = argparse.ArgumentParser(
+- description = 'Generate project data for use with the UI' )
+-
+- arg_parser.add_argument(
+- '--current-date',
+- help = ( "should be in the format rfc-3339 "
+- + "(e.g. 2006-08-07 12:34:56-06:00)" ),
+- required = True )
+- arg_parser.add_argument(
+- '--git-commit-date',
+- help = ( "should be in the format rfc-3339 "
+- + "(e.g. 2006-08-07 12:34:56-06:00)" ),
+- required = True )
+- arg_parser.add_argument(
+- '--git-commit-id',
+- help = "the git commit ID",
+- required = True )
+- arg_parser.add_argument(
+- '--map-file-path',
+- help = "the path to the '.map' file",
+- required = True )
+- arg_parser.add_argument(
+- '--source-code-path',
+- help = "the path to the source code directory",
+- required = True )
+- arg_parser.add_argument(
+- '--matrix-file-path',
+- help = "the path to the matrix file we're using",
+- required = True )
+- arg_parser.add_argument(
+- '--layout-file-path',
+- help = "the path to the layout file we're using",
+- required = True )
+-
+- args = arg_parser.parse_args(sys.argv[1:])
+-
+- output = {}
+- dict_merge( output, gen_static( args.current_date,
+- args.git_commit_date,
+- args.git_commit_id ) )
+- dict_merge(output, parse_mapfile(args.map_file_path))
+- dict_merge(output, find_keyboard_functions(args.source_code_path))
+- dict_merge(output, gen_mappings( args.matrix_file_path,
+- args.layout_file_path ))
+- dict_merge(output, gen_derived(output))
+-
+- print(json.dumps(output, sort_keys=True, indent=4))
++ arg_parser = argparse.ArgumentParser(
++ description="Generate project data for use with the UI"
++ )
++
++ arg_parser.add_argument(
++ "--current-date",
++ help=(
++ "should be in the format rfc-3339 "
++ "(e.g. 2006-08-07 12:34:56-06:00)"
++ ),
++ required=True,
++ )
++ arg_parser.add_argument(
++ "--git-commit-date",
++ help=(
++ "should be in the format rfc-3339 "
++ "(e.g. 2006-08-07 12:34:56-06:00)"
++ ),
++ required=True,
++ )
++ arg_parser.add_argument(
++ "--git-commit-id", help="the git commit ID", required=True
++ )
++ arg_parser.add_argument(
++ "--map-file-path", help="the path to the '.map' file", required=True
++ )
++ arg_parser.add_argument(
++ "--source-code-path",
++ help="the path to the source code directory",
++ required=True,
++ )
++ arg_parser.add_argument(
++ "--matrix-file-path",
++ help="the path to the matrix file we're using",
++ required=True,
++ )
++ arg_parser.add_argument(
++ "--layout-file-path",
++ help="the path to the layout file we're using",
++ required=True,
++ )
++
++ args = arg_parser.parse_args(sys.argv[1:])
++
++ output = {}
++ dict_merge(
++ output,
++ gen_static(
++ args.current_date, args.git_commit_date, args.git_commit_id
++ )
++ )
++ dict_merge(output, find_keyboard_functions(args.source_code_path))
++ dict_merge(
++ output, gen_mappings(args.matrix_file_path, args.layout_file_path)
++ )
++
++ print(json.dumps(output, sort_keys=True, indent=4))
+
+-# -----------------------------------------------------------------------------
+
+-if __name__ == '__main__':
+- main()
++# -----------------------------------------------------------------------------
+
++if __name__ == "__main__":
++ main()
+diff --git a/makefile b/makefile
+index d9fe10c..971ee0e 100644
+--- a/makefile
++++ b/makefile
+@@ -58,24 +58,27 @@ SCRIPTS := build-scripts
+ all: dist
+
+ clean:
+- git clean -dX # remove ignored files and directories
+- -rm -r '$(BUILD)'
++ git clean -fdX # remove ignored files and directories
++ rm -rf '$(BUILD)'
+
+ checkin:
+ -git commit -a
+
+ build-dir:
+- -rm -r '$(BUILD)/$(TARGET)'*
+- -mkdir -p '$(BUILD)/$(TARGET)'
++ rm -rf '$(BUILD)/$(TARGET)'*
++ mkdir -p '$(BUILD)/$(TARGET)'
+
+ firmware:
+ cd src; $(MAKE) LAYOUT=$(LAYOUT) all
+
+-$(ROOT)/firmware.%: firmware
++$(ROOT):
++ mkdir -p '$@'
++
++$(ROOT)/firmware.%: firmware $(ROOT)
+ cp 'src/firmware.$*' '$@'
+
+
+-$(ROOT)/firmware--ui-info.json: $(SCRIPTS)/gen-ui-info.py checkin
++$(ROOT)/firmware--ui-info.json: $(SCRIPTS)/gen-ui-info.py checkin firmware
+ ( ./'$<' \
+ --current-date '$(shell $(DATE_PROG) --rfc-3339 s)' \
+ --git-commit-date '$(GIT_COMMIT_DATE)' \
diff --git a/gnu/packages/patches/ergodox-firmware-fix-numpad.patch b/gnu/packages/patches/ergodox-firmware-fix-numpad.patch
new file mode 100644
index 0000000000..47af9f8398
--- /dev/null
+++ b/gnu/packages/patches/ergodox-firmware-fix-numpad.patch
@@ -0,0 +1,18 @@
+Submitted upstream: https://github.com/benblazak/ergodox-firmware/pull/100
+
+diff --git a/src/lib/key-functions/public/special.c b/src/lib/key-functions/public/special.c
+index 42aba45..6488137 100644
+--- a/src/lib/key-functions/public/special.c
++++ b/src/lib/key-functions/public/special.c
+@@ -102,9 +102,9 @@ void kbfun_2_keys_capslock_press_release(void) {
+ static uint8_t numpad_layer_id;
+
+ static inline void numpad_toggle_numlock(void) {
+- _kbfun_press_release(true, KEY_LockingNumLock);
++ _kbfun_press_release(true, KEYPAD_NumLock_Clear);
+ usb_keyboard_send();
+- _kbfun_press_release(false, KEY_LockingNumLock);
++ _kbfun_press_release(false, KEYPAD_NumLock_Clear);
+ usb_keyboard_send();
+ }
+