aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/c_parse.py30
-rw-r--r--lib/python/qmk/cli/__init__.py23
-rw-r--r--lib/python/qmk/cli/c2json.py3
-rw-r--r--lib/python/qmk/cli/chibios/confmigrate.py9
-rw-r--r--lib/python/qmk/cli/generate/__init__.py4
-rwxr-xr-xlib/python/qmk/cli/generate/api.py14
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py152
-rwxr-xr-xlib/python/qmk/cli/generate/info_json.py65
-rwxr-xr-xlib/python/qmk/cli/generate/layouts.py98
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py91
-rwxr-xr-xlib/python/qmk/cli/info.py7
-rw-r--r--lib/python/qmk/cli/json/__init__.py5
-rwxr-xr-xlib/python/qmk/cli/json/keymap.py16
-rwxr-xr-xlib/python/qmk/cli/kle2json.py51
-rw-r--r--lib/python/qmk/constants.py15
-rw-r--r--lib/python/qmk/info.py457
-rwxr-xr-xlib/python/qmk/info_json_encoder.py96
-rw-r--r--lib/python/qmk/os_helpers/linux/__init__.py1
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py29
19 files changed, 1001 insertions, 165 deletions
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index e41e271a4..89dd278b7 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -1,12 +1,27 @@
1"""Functions for working with config.h files. 1"""Functions for working with config.h files.
2""" 2"""
3from pathlib import Path 3from pathlib import Path
4import re
4 5
5from milc import cli 6from milc import cli
6 7
7from qmk.comment_remover import comment_remover 8from qmk.comment_remover import comment_remover
8 9
9default_key_entry = {'x': -1, 'y': 0, 'w': 1} 10default_key_entry = {'x': -1, 'y': 0, 'w': 1}
11single_comment_regex = re.compile(r' */[/*].*$')
12multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
13
14
15def strip_line_comment(string):
16 """Removes comments from a single line string.
17 """
18 return single_comment_regex.sub('', string)
19
20
21def strip_multiline_comment(string):
22 """Removes comments from a single line string.
23 """
24 return multi_comment_regex.sub('', string)
10 25
11 26
12def c_source_files(dir_names): 27def c_source_files(dir_names):
@@ -52,8 +67,11 @@ def find_layouts(file):
52 layout = layout.strip() 67 layout = layout.strip()
53 parsed_layout = [_default_key(key) for key in layout.split(',')] 68 parsed_layout = [_default_key(key) for key in layout.split(',')]
54 69
55 for key in parsed_layout: 70 for i, key in enumerate(parsed_layout):
56 key['matrix'] = matrix_locations.get(key['label']) 71 if 'label' not in key:
72 cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i)
73 elif key['label'] in matrix_locations:
74 key['matrix'] = matrix_locations[key['label']]
57 75
58 parsed_layouts[macro_name] = { 76 parsed_layouts[macro_name] = {
59 'key_count': len(parsed_layout), 77 'key_count': len(parsed_layout),
@@ -88,12 +106,10 @@ def parse_config_h_file(config_h_file, config_h=None):
88 if config_h_file.exists(): 106 if config_h_file.exists():
89 config_h_text = config_h_file.read_text() 107 config_h_text = config_h_file.read_text()
90 config_h_text = config_h_text.replace('\\\n', '') 108 config_h_text = config_h_text.replace('\\\n', '')
109 config_h_text = strip_multiline_comment(config_h_text)
91 110
92 for linenum, line in enumerate(config_h_text.split('\n')): 111 for linenum, line in enumerate(config_h_text.split('\n')):
93 line = line.strip() 112 line = strip_line_comment(line).strip()
94
95 if '//' in line:
96 line = line[:line.index('//')].strip()
97 113
98 if not line: 114 if not line:
99 continue 115 continue
@@ -156,6 +172,6 @@ def _parse_matrix_locations(matrix, file, macro_name):
156 row = row.replace('{', '').replace('}', '') 172 row = row.replace('{', '').replace('}', '')
157 for col_num, identifier in enumerate(row.split(',')): 173 for col_num, identifier in enumerate(row.split(',')):
158 if identifier != 'KC_NO': 174 if identifier != 'KC_NO':
159 matrix_locations[identifier] = (row_num, col_num) 175 matrix_locations[identifier] = [row_num, col_num]
160 176
161 return matrix_locations 177 return matrix_locations
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 372c40921..a5f1f4767 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -19,7 +19,6 @@ from . import flash
19from . import generate 19from . import generate
20from . import hello 20from . import hello
21from . import info 21from . import info
22from . import json
23from . import json2c 22from . import json2c
24from . import lint 23from . import lint
25from . import list 24from . import list
@@ -28,6 +27,24 @@ from . import new
28from . import pyformat 27from . import pyformat
29from . import pytest 28from . import pytest
30 29
31if sys.version_info[0] != 3 or sys.version_info[1] < 6: 30# Supported version information
32 cli.log.error('Your Python is too old! Please upgrade to Python 3.6 or later.') 31#
32# Based on the OSes we support these are the minimum python version available by default.
33# Last update: 2021 Jan 02
34#
35# Arch: 3.9
36# Debian: 3.7
37# Fedora 31: 3.7
38# Fedora 32: 3.8
39# Fedora 33: 3.9
40# FreeBSD: 3.7
41# Gentoo: 3.7
42# macOS: 3.9 (from homebrew)
43# msys2: 3.8
44# Slackware: 3.7
45# solus: 3.7
46# void: 3.9
47
48if sys.version_info[0] != 3 or sys.version_info[1] < 7:
49 cli.log.error('Your Python is too old! Please upgrade to Python 3.7 or later.')
33 exit(127) 50 exit(127)
diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py
index 2b3bb774f..8f9d8dc38 100644
--- a/lib/python/qmk/cli/c2json.py
+++ b/lib/python/qmk/cli/c2json.py
@@ -6,6 +6,7 @@ from milc import cli
6 6
7import qmk.keymap 7import qmk.keymap
8import qmk.path 8import qmk.path
9from qmk.info_json_encoder import InfoJSONEncoder
9 10
10 11
11@cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c') 12@cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c')
@@ -47,7 +48,7 @@ def c2json(cli):
47 cli.args.output.parent.mkdir(parents=True, exist_ok=True) 48 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
48 if cli.args.output.exists(): 49 if cli.args.output.exists():
49 cli.args.output.replace(cli.args.output.name + '.bak') 50 cli.args.output.replace(cli.args.output.name + '.bak')
50 cli.args.output.write_text(json.dumps(keymap_json)) 51 cli.args.output.write_text(json.dumps(keymap_json, cls=InfoJSONEncoder))
51 52
52 if not cli.args.quiet: 53 if not cli.args.quiet:
53 cli.log.info('Wrote keymap to %s.', cli.args.output) 54 cli.log.info('Wrote keymap to %s.', cli.args.output)
diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py
index b9cfda961..3e348b2b0 100644
--- a/lib/python/qmk/cli/chibios/confmigrate.py
+++ b/lib/python/qmk/cli/chibios/confmigrate.py
@@ -32,7 +32,7 @@ file_header = """\
32 32
33/* 33/*
34 * This file was auto-generated by: 34 * This file was auto-generated by:
35 * `qmk chibios-confupdate -i {0} -r {1}` 35 * `qmk chibios-confmigrate -i {0} -r {1}`
36 */ 36 */
37 37
38#pragma once 38#pragma once
@@ -111,6 +111,7 @@ def migrate_mcuconf_h(to_override, outfile):
111@cli.argument('-r', '--reference', type=normpath, arg_only=True, help='Specify the reference file to compare against') 111@cli.argument('-r', '--reference', type=normpath, arg_only=True, help='Specify the reference file to compare against')
112@cli.argument('-o', '--overwrite', arg_only=True, action='store_true', help='Overwrites the input file during migration.') 112@cli.argument('-o', '--overwrite', arg_only=True, action='store_true', help='Overwrites the input file during migration.')
113@cli.argument('-d', '--delete', arg_only=True, action='store_true', help='If the file has no overrides, migration will delete the input file.') 113@cli.argument('-d', '--delete', arg_only=True, action='store_true', help='If the file has no overrides, migration will delete the input file.')
114@cli.argument('-f', '--force', arg_only=True, action='store_true', help='Re-migrates an already migrated file, even if it doesn\'t detect a full ChibiOS config.')
114@cli.subcommand('Generates a migrated ChibiOS configuration file, as a result of comparing the input against a reference') 115@cli.subcommand('Generates a migrated ChibiOS configuration file, as a result of comparing the input against a reference')
115def chibios_confmigrate(cli): 116def chibios_confmigrate(cli):
116 """Generates a usable ChibiOS replacement configuration file, based on a fully-defined conf and a reference config. 117 """Generates a usable ChibiOS replacement configuration file, based on a fully-defined conf and a reference config.
@@ -142,19 +143,19 @@ def chibios_confmigrate(cli):
142 143
143 eprint('--------------------------------------') 144 eprint('--------------------------------------')
144 145
145 if "CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"]: 146 if cli.args.input.name == "chconf.h" and ("CHCONF_H" in input_defs["dict"] or "_CHCONF_H_" in input_defs["dict"] or cli.args.force):
146 migrate_chconf_h(to_override, outfile=sys.stdout) 147 migrate_chconf_h(to_override, outfile=sys.stdout)
147 if cli.args.overwrite: 148 if cli.args.overwrite:
148 with open(cli.args.input, "w") as out_file: 149 with open(cli.args.input, "w") as out_file:
149 migrate_chconf_h(to_override, outfile=out_file) 150 migrate_chconf_h(to_override, outfile=out_file)
150 151
151 elif "HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"]: 152 elif cli.args.input.name == "halconf.h" and ("HALCONF_H" in input_defs["dict"] or "_HALCONF_H_" in input_defs["dict"] or cli.args.force):
152 migrate_halconf_h(to_override, outfile=sys.stdout) 153 migrate_halconf_h(to_override, outfile=sys.stdout)
153 if cli.args.overwrite: 154 if cli.args.overwrite:
154 with open(cli.args.input, "w") as out_file: 155 with open(cli.args.input, "w") as out_file:
155 migrate_halconf_h(to_override, outfile=out_file) 156 migrate_halconf_h(to_override, outfile=out_file)
156 157
157 elif "MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"]: 158 elif cli.args.input.name == "mcuconf.h" and ("MCUCONF_H" in input_defs["dict"] or "_MCUCONF_H_" in input_defs["dict"] or cli.args.force):
158 migrate_mcuconf_h(to_override, outfile=sys.stdout) 159 migrate_mcuconf_h(to_override, outfile=sys.stdout)
159 if cli.args.overwrite: 160 if cli.args.overwrite:
160 with open(cli.args.input, "w") as out_file: 161 with open(cli.args.input, "w") as out_file:
diff --git a/lib/python/qmk/cli/generate/__init__.py b/lib/python/qmk/cli/generate/__init__.py
index f9585bfb5..bd75b044c 100644
--- a/lib/python/qmk/cli/generate/__init__.py
+++ b/lib/python/qmk/cli/generate/__init__.py
@@ -1,3 +1,7 @@
1from . import api 1from . import api
2from . import config_h
2from . import docs 3from . import docs
4from . import info_json
5from . import layouts
3from . import rgb_breathe_table 6from . import rgb_breathe_table
7from . import rules_mk
diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py
index 66db37cb5..6d111f244 100755
--- a/lib/python/qmk/cli/generate/api.py
+++ b/lib/python/qmk/cli/generate/api.py
@@ -8,6 +8,7 @@ from milc import cli
8 8
9from qmk.datetime import current_datetime 9from qmk.datetime import current_datetime
10from qmk.info import info_json 10from qmk.info import info_json
11from qmk.info_json_encoder import InfoJSONEncoder
11from qmk.keyboard import list_keyboards 12from qmk.keyboard import list_keyboards
12 13
13 14
@@ -44,15 +45,16 @@ def generate_api(cli):
44 if 'usb' in kb_all['keyboards'][keyboard_name]: 45 if 'usb' in kb_all['keyboards'][keyboard_name]:
45 usb = kb_all['keyboards'][keyboard_name]['usb'] 46 usb = kb_all['keyboards'][keyboard_name]['usb']
46 47
47 if usb['vid'] not in usb_list['devices']: 48 if 'vid' in usb and usb['vid'] not in usb_list['devices']:
48 usb_list['devices'][usb['vid']] = {} 49 usb_list['devices'][usb['vid']] = {}
49 50
50 if usb['pid'] not in usb_list['devices'][usb['vid']]: 51 if 'pid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]:
51 usb_list['devices'][usb['vid']][usb['pid']] = {} 52 usb_list['devices'][usb['vid']][usb['pid']] = {}
52 53
53 usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb 54 if 'vid' in usb and 'pid' in usb:
55 usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb
54 56
55 # Write the global JSON files 57 # Write the global JSON files
56 keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])})) 58 keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])}, cls=InfoJSONEncoder))
57 keyboard_all.write_text(json.dumps(kb_all)) 59 keyboard_all.write_text(json.dumps(kb_all, cls=InfoJSONEncoder))
58 usb_file.write_text(json.dumps(usb_list)) 60 usb_file.write_text(json.dumps(usb_list, cls=InfoJSONEncoder))
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
new file mode 100755
index 000000000..7ddad745d
--- /dev/null
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -0,0 +1,152 @@
1"""Used by the make system to generate info_config.h from info.json.
2"""
3from pathlib import Path
4
5from dotty_dict import dotty
6from milc import cli
7
8from qmk.decorators import automagic_keyboard, automagic_keymap
9from qmk.info import _json_load, info_json
10from qmk.path import is_keyboard, normpath
11
12
13def direct_pins(direct_pins):
14 """Return the config.h lines that set the direct pins.
15 """
16 rows = []
17
18 for row in direct_pins:
19 cols = ','.join(map(str, [col or 'NO_PIN' for col in row]))
20 rows.append('{' + cols + '}')
21
22 col_count = len(direct_pins[0])
23 row_count = len(direct_pins)
24
25 return """
26#ifndef MATRIX_COLS
27# define MATRIX_COLS %s
28#endif // MATRIX_COLS
29
30#ifndef MATRIX_ROWS
31# define MATRIX_ROWS %s
32#endif // MATRIX_ROWS
33
34#ifndef DIRECT_PINS
35# define DIRECT_PINS {%s}
36#endif // DIRECT_PINS
37""" % (col_count, row_count, ','.join(rows))
38
39
40def pin_array(define, pins):
41 """Return the config.h lines that set a pin array.
42 """
43 pin_num = len(pins)
44 pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
45
46 return f"""
47#ifndef {define}S
48# define {define}S {pin_num}
49#endif // {define}S
50
51#ifndef {define}_PINS
52# define {define}_PINS {{ {pin_array} }}
53#endif // {define}_PINS
54"""
55
56
57def matrix_pins(matrix_pins):
58 """Add the matrix config to the config.h.
59 """
60 pins = []
61
62 if 'direct' in matrix_pins:
63 pins.append(direct_pins(matrix_pins['direct']))
64
65 if 'cols' in matrix_pins:
66 pins.append(pin_array('MATRIX_COL', matrix_pins['cols']))
67
68 if 'rows' in matrix_pins:
69 pins.append(pin_array('MATRIX_ROW', matrix_pins['rows']))
70
71 return '\n'.join(pins)
72
73
74@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
75@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
76@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
77@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
78@automagic_keyboard
79@automagic_keymap
80def generate_config_h(cli):
81 """Generates the info_config.h file.
82 """
83 # Determine our keyboard(s)
84 if not cli.config.generate_config_h.keyboard:
85 cli.log.error('Missing paramater: --keyboard')
86 cli.subcommands['info'].print_help()
87 return False
88
89 if not is_keyboard(cli.config.generate_config_h.keyboard):
90 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
91 return False
92
93 # Build the info_config.h file.
94 kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
95 info_config_map = _json_load(Path('data/mappings/info_config.json'))
96
97 config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
98
99 # Iterate through the info_config map to generate basic things
100 for config_key, info_dict in info_config_map.items():
101 info_key = info_dict['info_key']
102 key_type = info_dict.get('value_type', 'str')
103 to_config = info_dict.get('to_config', True)
104
105 if not to_config:
106 continue
107
108 try:
109 config_value = kb_info_json[info_key]
110 except KeyError:
111 continue
112
113 if key_type.startswith('array'):
114 config_h_lines.append('')
115 config_h_lines.append(f'#ifndef {config_key}')
116 config_h_lines.append(f'# define {config_key} {{ {", ".join(map(str, config_value))} }}')
117 config_h_lines.append(f'#endif // {config_key}')
118 elif key_type == 'bool':
119 if config_value:
120 config_h_lines.append('')
121 config_h_lines.append(f'#ifndef {config_key}')
122 config_h_lines.append(f'# define {config_key}')
123 config_h_lines.append(f'#endif // {config_key}')
124 elif key_type == 'mapping':
125 for key, value in config_value.items():
126 config_h_lines.append('')
127 config_h_lines.append(f'#ifndef {key}')
128 config_h_lines.append(f'# define {key} {value}')
129 config_h_lines.append(f'#endif // {key}')
130 else:
131 config_h_lines.append('')
132 config_h_lines.append(f'#ifndef {config_key}')
133 config_h_lines.append(f'# define {config_key} {config_value}')
134 config_h_lines.append(f'#endif // {config_key}')
135
136 if 'matrix_pins' in kb_info_json:
137 config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
138
139 # Show the results
140 config_h = '\n'.join(config_h_lines)
141
142 if cli.args.output:
143 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
144 if cli.args.output.exists():
145 cli.args.output.replace(cli.args.output.name + '.bak')
146 cli.args.output.write_text(config_h)
147
148 if not cli.args.quiet:
149 cli.log.info('Wrote info_config.h to %s.', cli.args.output)
150
151 else:
152 print(config_h)
diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
new file mode 100755
index 000000000..f3fc54ddc
--- /dev/null
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -0,0 +1,65 @@
1"""Keyboard information script.
2
3Compile an info.json for a particular keyboard and pretty-print it.
4"""
5import json
6
7from jsonschema import Draft7Validator, validators
8from milc import cli
9
10from qmk.decorators import automagic_keyboard, automagic_keymap
11from qmk.info import info_json, _jsonschema
12from qmk.info_json_encoder import InfoJSONEncoder
13from qmk.path import is_keyboard
14
15
16def pruning_validator(validator_class):
17 """Extends Draft7Validator to remove properties that aren't specified in the schema.
18 """
19 validate_properties = validator_class.VALIDATORS["properties"]
20
21 def remove_additional_properties(validator, properties, instance, schema):
22 for prop in list(instance.keys()):
23 if prop not in properties:
24 del instance[prop]
25
26 for error in validate_properties(validator, properties, instance, schema):
27 yield error
28
29 return validators.extend(validator_class, {"properties": remove_additional_properties})
30
31
32def strip_info_json(kb_info_json):
33 """Remove the API-only properties from the info.json.
34 """
35 pruning_draft_7_validator = pruning_validator(Draft7Validator)
36 schema = _jsonschema('keyboard')
37 validator = pruning_draft_7_validator(schema).validate
38
39 return validator(kb_info_json)
40
41
42@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')
43@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
44@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
45@automagic_keyboard
46@automagic_keymap
47def generate_info_json(cli):
48 """Generate an info.json file for a keyboard
49 """
50 # Determine our keyboard(s)
51 if not cli.config.generate_info_json.keyboard:
52 cli.log.error('Missing parameter: --keyboard')
53 cli.subcommands['info'].print_help()
54 return False
55
56 if not is_keyboard(cli.config.generate_info_json.keyboard):
57 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
58 return False
59
60 # Build the info.json file
61 kb_info_json = info_json(cli.config.generate_info_json.keyboard)
62 strip_info_json(kb_info_json)
63
64 # Display the results
65 print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder))
diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py
new file mode 100755
index 000000000..b7baae065
--- /dev/null
+++ b/lib/python/qmk/cli/generate/layouts.py
@@ -0,0 +1,98 @@
1"""Used by the make system to generate layouts.h from info.json.
2"""
3from milc import cli
4
5from qmk.constants import COL_LETTERS, ROW_LETTERS
6from qmk.decorators import automagic_keyboard, automagic_keymap
7from qmk.info import info_json
8from qmk.path import is_keyboard, normpath
9
10usb_properties = {
11 'vid': 'VENDOR_ID',
12 'pid': 'PRODUCT_ID',
13 'device_ver': 'DEVICE_VER',
14}
15
16
17@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
18@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
19@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
20@cli.subcommand('Used by the make system to generate layouts.h from info.json', hidden=True)
21@automagic_keyboard
22@automagic_keymap
23def generate_layouts(cli):
24 """Generates the layouts.h file.
25 """
26 # Determine our keyboard(s)
27 if not cli.config.generate_layouts.keyboard:
28 cli.log.error('Missing paramater: --keyboard')
29 cli.subcommands['info'].print_help()
30 return False
31
32 if not is_keyboard(cli.config.generate_layouts.keyboard):
33 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_layouts.keyboard)
34 return False
35
36 # Build the info.json file
37 kb_info_json = info_json(cli.config.generate_layouts.keyboard)
38
39 # Build the layouts.h file.
40 layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.' ' */', '', '#pragma once']
41
42 if 'matrix_pins' in kb_info_json:
43 if 'direct' in kb_info_json['matrix_pins']:
44 col_num = len(kb_info_json['matrix_pins']['direct'][0])
45 row_num = len(kb_info_json['matrix_pins']['direct'])
46 elif 'cols' in kb_info_json['matrix_pins'] and 'rows' in kb_info_json['matrix_pins']:
47 col_num = len(kb_info_json['matrix_pins']['cols'])
48 row_num = len(kb_info_json['matrix_pins']['rows'])
49 else:
50 cli.log.error('%s: Invalid matrix config.', cli.config.generate_layouts.keyboard)
51 return False
52
53 for layout_name in kb_info_json['layouts']:
54 if kb_info_json['layouts'][layout_name]['c_macro']:
55 continue
56
57 if 'matrix' not in kb_info_json['layouts'][layout_name]['layout'][0]:
58 cli.log.debug('%s/%s: No matrix data!', cli.config.generate_layouts.keyboard, layout_name)
59 continue
60
61 layout_keys = []
62 layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)]
63
64 for i, key in enumerate(kb_info_json['layouts'][layout_name]['layout']):
65 row = key['matrix'][0]
66 col = key['matrix'][1]
67 identifier = 'k%s%s' % (ROW_LETTERS[row], COL_LETTERS[col])
68
69 try:
70 layout_matrix[row][col] = identifier
71 layout_keys.append(identifier)
72 except IndexError:
73 key_name = key.get('label', identifier)
74 cli.log.error('Matrix data out of bounds for layout %s at index %s (%s): %s, %s', layout_name, i, key_name, row, col)
75 return False
76
77 layouts_h_lines.append('')
78 layouts_h_lines.append('#define %s(%s) {\\' % (layout_name, ', '.join(layout_keys)))
79
80 rows = ', \\\n'.join(['\t {' + ', '.join(row) + '}' for row in layout_matrix])
81 rows += ' \\'
82 layouts_h_lines.append(rows)
83 layouts_h_lines.append('}')
84
85 # Show the results
86 layouts_h = '\n'.join(layouts_h_lines) + '\n'
87
88 if cli.args.output:
89 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
90 if cli.args.output.exists():
91 cli.args.output.replace(cli.args.output.name + '.bak')
92 cli.args.output.write_text(layouts_h)
93
94 if not cli.args.quiet:
95 cli.log.info('Wrote info_config.h to %s.', cli.args.output)
96
97 else:
98 print(layouts_h)
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
new file mode 100755
index 000000000..af740f341
--- /dev/null
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -0,0 +1,91 @@
1"""Used by the make system to generate a rules.mk
2"""
3from pathlib import Path
4
5from dotty_dict import dotty
6from milc import cli
7
8from qmk.decorators import automagic_keyboard, automagic_keymap
9from qmk.info import _json_load, info_json
10from qmk.path import is_keyboard, normpath
11
12
13def process_mapping_rule(kb_info_json, rules_key, info_dict):
14 """Return the rules.mk line(s) for a mapping rule.
15 """
16 if not info_dict.get('to_c', True):
17 return None
18
19 info_key = info_dict['info_key']
20 key_type = info_dict.get('value_type', 'str')
21
22 try:
23 rules_value = kb_info_json[info_key]
24 except KeyError:
25 return None
26
27 if key_type == 'array':
28 return f'{rules_key} ?= {" ".join(rules_value)}'
29 elif key_type == 'bool':
30 return f'{rules_key} ?= {"on" if rules_value else "off"}'
31 elif key_type == 'mapping':
32 return '\n'.join([f'{key} ?= {value}' for key, value in rules_value.items()])
33
34 return f'{rules_key} ?= {rules_value}'
35
36
37@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
38@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
39@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
40@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
41@automagic_keyboard
42@automagic_keymap
43def generate_rules_mk(cli):
44 """Generates a rules.mk file from info.json.
45 """
46 if not cli.config.generate_rules_mk.keyboard:
47 cli.log.error('Missing paramater: --keyboard')
48 cli.subcommands['info'].print_help()
49 return False
50
51 if not is_keyboard(cli.config.generate_rules_mk.keyboard):
52 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard)
53 return False
54
55 kb_info_json = dotty(info_json(cli.config.generate_rules_mk.keyboard))
56 info_rules_map = _json_load(Path('data/mappings/info_rules.json'))
57 rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
58
59 # Iterate through the info_rules map to generate basic rules
60 for rules_key, info_dict in info_rules_map.items():
61 new_entry = process_mapping_rule(kb_info_json, rules_key, info_dict)
62
63 if new_entry:
64 rules_mk_lines.append(new_entry)
65
66 # Iterate through features to enable/disable them
67 if 'features' in kb_info_json:
68 for feature, enabled in kb_info_json['features'].items():
69 if feature == 'bootmagic_lite' and enabled:
70 rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite')
71 else:
72 feature = feature.upper()
73 enabled = 'yes' if enabled else 'no'
74 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
75
76 # Show the results
77 rules_mk = '\n'.join(rules_mk_lines) + '\n'
78
79 if cli.args.output:
80 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
81 if cli.args.output.exists():
82 cli.args.output.replace(cli.args.output.name + '.bak')
83 cli.args.output.write_text(rules_mk)
84
85 if cli.args.quiet:
86 print(cli.args.output)
87 else:
88 cli.log.info('Wrote rules.mk to %s.', cli.args.output)
89
90 else:
91 print(rules_mk)
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 9ab299a21..87d7253d4 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -7,6 +7,8 @@ import platform
7 7
8from milc import cli 8from milc import cli
9 9
10from qmk.info_json_encoder import InfoJSONEncoder
11from qmk.constants import COL_LETTERS, ROW_LETTERS
10from qmk.decorators import automagic_keyboard, automagic_keymap 12from qmk.decorators import automagic_keyboard, automagic_keymap
11from qmk.keyboard import render_layouts, render_layout 13from qmk.keyboard import render_layouts, render_layout
12from qmk.keymap import locate_keymap 14from qmk.keymap import locate_keymap
@@ -15,9 +17,6 @@ from qmk.path import is_keyboard
15 17
16platform_id = platform.platform().lower() 18platform_id = platform.platform().lower()
17 19
18ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
19COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
20
21 20
22def show_keymap(kb_info_json, title_caps=True): 21def show_keymap(kb_info_json, title_caps=True):
23 """Render the keymap in ascii art. 22 """Render the keymap in ascii art.
@@ -149,7 +148,7 @@ def info(cli):
149 148
150 # Output in the requested format 149 # Output in the requested format
151 if cli.args.format == 'json': 150 if cli.args.format == 'json':
152 print(json.dumps(kb_info_json)) 151 print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
153 elif cli.args.format == 'text': 152 elif cli.args.format == 'text':
154 print_text_output(kb_info_json) 153 print_text_output(kb_info_json)
155 elif cli.args.format == 'friendly': 154 elif cli.args.format == 'friendly':
diff --git a/lib/python/qmk/cli/json/__init__.py b/lib/python/qmk/cli/json/__init__.py
deleted file mode 100644
index f4ebfc45b..000000000
--- a/lib/python/qmk/cli/json/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
1"""QMK CLI JSON Subcommands
2
3We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
4"""
5from . import keymap
diff --git a/lib/python/qmk/cli/json/keymap.py b/lib/python/qmk/cli/json/keymap.py
deleted file mode 100755
index 2af9faaa7..000000000
--- a/lib/python/qmk/cli/json/keymap.py
+++ /dev/null
@@ -1,16 +0,0 @@
1"""Generate a keymap.c from a configurator export.
2"""
3from pathlib import Path
4
5from milc import cli
6
7
8@cli.argument('-o', '--output', arg_only=True, type=Path, help='File to write to')
9@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
10@cli.argument('filename', arg_only=True, help='Configurator JSON file')
11@cli.subcommand('Creates a keymap.c from a QMK Configurator export.', hidden=True)
12def json_keymap(cli):
13 """Renamed to `qmk json2c`.
14 """
15 cli.log.error('This command has been renamed to `qmk json2c`.')
16 return False
diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py
index 3d1bb8c43..66d504bfc 100755
--- a/lib/python/qmk/cli/kle2json.py
+++ b/lib/python/qmk/cli/kle2json.py
@@ -3,25 +3,12 @@
3import json 3import json
4import os 4import os
5from pathlib import Path 5from pathlib import Path
6from decimal import Decimal
7from collections import OrderedDict
8 6
9from milc import cli 7from milc import cli
10from kle2xy import KLE2xy 8from kle2xy import KLE2xy
11 9
12from qmk.converter import kle2qmk 10from qmk.converter import kle2qmk
13 11from qmk.info_json_encoder import InfoJSONEncoder
14
15class CustomJSONEncoder(json.JSONEncoder):
16 def default(self, obj):
17 try:
18 if isinstance(obj, Decimal):
19 if obj % 2 in (Decimal(0), Decimal(1)):
20 return int(obj)
21 return float(obj)
22 except TypeError:
23 pass
24 return json.JSONEncoder.default(self, obj)
25 12
26 13
27@cli.argument('filename', help='The KLE raw txt to convert') 14@cli.argument('filename', help='The KLE raw txt to convert')
@@ -52,24 +39,22 @@ def kle2json(cli):
52 cli.log.error('Could not parse KLE raw data: %s', raw_code) 39 cli.log.error('Could not parse KLE raw data: %s', raw_code)
53 cli.log.exception(e) 40 cli.log.exception(e)
54 return False 41 return False
55 keyboard = OrderedDict( 42 keyboard = {
56 keyboard_name=kle.name, 43 'keyboard_name': kle.name,
57 url='', 44 'url': '',
58 maintainer='qmk', 45 'maintainer': 'qmk',
59 width=kle.columns, 46 'width': kle.columns,
60 height=kle.rows, 47 'height': kle.rows,
61 layouts={'LAYOUT': { 48 'layouts': {
62 'layout': 'LAYOUT_JSON_HERE' 49 'LAYOUT': {
63 }}, 50 'layout': kle2qmk(kle)
64 ) 51 }
65 # Initialize keyboard with json encoded from ordered dict 52 },
66 keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=CustomJSONEncoder) 53 }
67 # Initialize layout with kle2qmk from converter module 54
68 layout = json.dumps(kle2qmk(kle), separators=(', ', ':'), cls=CustomJSONEncoder)
69 # Replace layout in keyboard json
70 keyboard = keyboard.replace('"LAYOUT_JSON_HERE"', layout)
71 # Write our info.json 55 # Write our info.json
72 file = open(out_path / "info.json", "w") 56 keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=InfoJSONEncoder)
73 file.write(keyboard) 57 info_json_file = out_path / 'info.json'
74 file.close() 58
59 info_json_file.write_text(keyboard)
75 cli.log.info('Wrote out {fg_cyan}%s/info.json', out_path) 60 cli.log.info('Wrote out {fg_cyan}%s/info.json', out_path)
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 2ddaa568a..3ed69f3bf 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -10,8 +10,8 @@ QMK_FIRMWARE = Path.cwd()
10MAX_KEYBOARD_SUBFOLDERS = 5 10MAX_KEYBOARD_SUBFOLDERS = 5
11 11
12# Supported processor types 12# Supported processor types
13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411' 13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32G431', 'STM32G474'
14LUFA_PROCESSORS = 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None 14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' 15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
16 16
17# Common format strings 17# Common format strings
@@ -19,6 +19,17 @@ DATE_FORMAT = '%Y-%m-%d'
19DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z' 19DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
20TIME_FORMAT = '%H:%M:%S' 20TIME_FORMAT = '%H:%M:%S'
21 21
22# Used when generating matrix locations
23COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
24ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
25
26# Mapping between info.json and config.h keys
27LED_INDICATORS = {
28 'caps_lock': 'LED_CAPS_LOCK_PIN',
29 'num_lock': 'LED_NUM_LOCK_PIN',
30 'scroll_lock': 'LED_SCROLL_LOCK_PIN',
31}
32
22# Constants that should match their counterparts in make 33# Constants that should match their counterparts in make
23BUILD_DIR = environ.get('BUILD_DIR', '.build') 34BUILD_DIR = environ.get('BUILD_DIR', '.build')
24KEYBOARD_OUTPUT_PREFIX = f'{BUILD_DIR}/obj_' 35KEYBOARD_OUTPUT_PREFIX = f'{BUILD_DIR}/obj_'
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index f476dc666..2accaba9e 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -1,9 +1,13 @@
1"""Functions that help us generate and use info.json files. 1"""Functions that help us generate and use info.json files.
2""" 2"""
3import json 3import json
4from collections.abc import Mapping
4from glob import glob 5from glob import glob
5from pathlib import Path 6from pathlib import Path
6 7
8import hjson
9import jsonschema
10from dotty_dict import dotty
7from milc import cli 11from milc import cli
8 12
9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 13from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
@@ -13,6 +17,9 @@ from qmk.keymap import list_keymaps
13from qmk.makefile import parse_rules_mk_file 17from qmk.makefile import parse_rules_mk_file
14from qmk.math import compute 18from qmk.math import compute
15 19
20true_values = ['1', 'on', 'yes']
21false_values = ['0', 'off', 'no']
22
16 23
17def info_json(keyboard): 24def info_json(keyboard):
18 """Generate the info.json data for a specific keyboard. 25 """Generate the info.json data for a specific keyboard.
@@ -38,8 +45,9 @@ def info_json(keyboard):
38 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} 45 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
39 46
40 # Populate layout data 47 # Populate layout data
41 for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items(): 48 for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items():
42 if not layout_name.startswith('LAYOUT_kc'): 49 if not layout_name.startswith('LAYOUT_kc'):
50 layout_json['c_macro'] = True
43 info_data['layouts'][layout_name] = layout_json 51 info_data['layouts'][layout_name] = layout_json
44 52
45 # Merge in the data from info.json, config.h, and rules.mk 53 # Merge in the data from info.json, config.h, and rules.mk
@@ -47,54 +55,259 @@ def info_json(keyboard):
47 info_data = _extract_config_h(info_data) 55 info_data = _extract_config_h(info_data)
48 info_data = _extract_rules_mk(info_data) 56 info_data = _extract_rules_mk(info_data)
49 57
58 # Validate against the jsonschema
59 try:
60 keyboard_api_validate(info_data)
61
62 except jsonschema.ValidationError as e:
63 json_path = '.'.join([str(p) for p in e.absolute_path])
64 cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
65 exit()
66
67 # Make sure we have at least one layout
68 if not info_data.get('layouts'):
69 _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
70
71 # Make sure we supply layout macros for the community layouts we claim to support
72 # FIXME(skullydazed): This should be populated into info.json and read from there instead
73 if 'LAYOUTS' in rules and info_data.get('layouts'):
74 # Match these up against the supplied layouts
75 supported_layouts = rules['LAYOUTS'].strip().split()
76 for layout_name in sorted(info_data['layouts']):
77 layout_name = layout_name[7:]
78
79 if layout_name in supported_layouts:
80 supported_layouts.remove(layout_name)
81
82 if supported_layouts:
83 for supported_layout in supported_layouts:
84 _log_error(info_data, 'Claims to support community layout %s but no LAYOUT_%s() macro found' % (supported_layout, supported_layout))
85
50 return info_data 86 return info_data
51 87
52 88
53def _extract_config_h(info_data): 89def _json_load(json_file):
54 """Pull some keyboard information from existing rules.mk files 90 """Load a json file from disk.
91
92 Note: file must be a Path object.
93 """
94 try:
95 return hjson.load(json_file.open())
96
97 except json.decoder.JSONDecodeError as e:
98 cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
99 exit(1)
100
101
102def _jsonschema(schema_name):
103 """Read a jsonschema file from disk.
104
105 FIXME(skullydazed/anyone): Refactor to make this a public function.
106 """
107 schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
108
109 if not schema_path.exists():
110 schema_path = Path('data/schemas/false.jsonschema')
111
112 return _json_load(schema_path)
113
114
115def keyboard_validate(data):
116 """Validates data against the keyboard jsonschema.
117 """
118 schema = _jsonschema('keyboard')
119 validator = jsonschema.Draft7Validator(schema).validate
120
121 return validator(data)
122
123
124def keyboard_api_validate(data):
125 """Validates data against the api_keyboard jsonschema.
126 """
127 base = _jsonschema('keyboard')
128 relative = _jsonschema('api_keyboard')
129 resolver = jsonschema.RefResolver.from_schema(base)
130 validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
131
132 return validator(data)
133
134
135def _extract_features(info_data, rules):
136 """Find all the features enabled in rules.mk.
137 """
138 # Special handling for bootmagic which also supports a "lite" mode.
139 if rules.get('BOOTMAGIC_ENABLE') == 'lite':
140 rules['BOOTMAGIC_LITE_ENABLE'] = 'on'
141 del rules['BOOTMAGIC_ENABLE']
142 if rules.get('BOOTMAGIC_ENABLE') == 'full':
143 rules['BOOTMAGIC_ENABLE'] = 'on'
144
145 # Skip non-boolean features we haven't implemented special handling for
146 for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
147 if rules.get(feature):
148 del rules[feature]
149
150 # Process the rest of the rules as booleans
151 for key, value in rules.items():
152 if key.endswith('_ENABLE'):
153 key = '_'.join(key.split('_')[:-1]).lower()
154 value = True if value.lower() in true_values else False if value.lower() in false_values else value
155
156 if 'config_h_features' not in info_data:
157 info_data['config_h_features'] = {}
158
159 if 'features' not in info_data:
160 info_data['features'] = {}
161
162 if key in info_data['features']:
163 _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
164
165 info_data['features'][key] = value
166 info_data['config_h_features'][key] = value
167
168 return info_data
169
170
171def _pin_name(pin):
172 """Returns the proper representation for a pin.
173 """
174 pin = pin.strip()
175
176 if not pin:
177 return None
178
179 elif pin.isdigit():
180 return int(pin)
181
182 elif pin == 'NO_PIN':
183 return None
184
185 elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit():
186 return pin
187
188 raise ValueError(f'Invalid pin: {pin}')
189
190
191def _extract_pins(pins):
192 """Returns a list of pins from a comma separated string of pins.
193 """
194 return [_pin_name(pin) for pin in pins.split(',')]
195
196
197def _extract_direct_matrix(info_data, direct_pins):
198 """
199 """
200 info_data['matrix_pins'] = {}
201 direct_pin_array = []
202
203 while direct_pins[-1] != '}':
204 direct_pins = direct_pins[:-1]
205
206 for row in direct_pins.split('},{'):
207 if row.startswith('{'):
208 row = row[1:]
209
210 if row.endswith('}'):
211 row = row[:-1]
212
213 direct_pin_array.append([])
214
215 for pin in row.split(','):
216 if pin == 'NO_PIN':
217 pin = None
218
219 direct_pin_array[-1].append(pin)
220
221 return direct_pin_array
222
223
224def _extract_matrix_info(info_data, config_c):
225 """Populate the matrix information.
55 """ 226 """
56 config_c = config_h(info_data['keyboard_folder'])
57 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() 227 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
58 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() 228 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
59 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] 229 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
60 230
61 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION') 231 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
62 info_data['matrix_size'] = { 232 if 'matrix_size' in info_data:
63 'rows': compute(config_c.get('MATRIX_ROWS', '0')), 233 _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
64 'cols': compute(config_c.get('MATRIX_COLS', '0')), 234
65 } 235 info_data['matrix_size'] = {
66 info_data['matrix_pins'] = {} 236 'cols': compute(config_c.get('MATRIX_COLS', '0')),
237 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
238 }
67 239
68 if row_pins: 240 if row_pins and col_pins:
69 info_data['matrix_pins']['rows'] = row_pins.split(',') 241 if 'matrix_pins' in info_data:
70 if col_pins: 242 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
71 info_data['matrix_pins']['cols'] = col_pins.split(',') 243
244 info_data['matrix_pins'] = {
245 'cols': _extract_pins(col_pins),
246 'rows': _extract_pins(row_pins),
247 }
72 248
73 if direct_pins: 249 if direct_pins:
74 direct_pin_array = [] 250 if 'matrix_pins' in info_data:
75 for row in direct_pins.split('},{'): 251 _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
76 if row.startswith('{'):
77 row = row[1:]
78 if row.endswith('}'):
79 row = row[:-1]
80 252
81 direct_pin_array.append([]) 253 info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
82 254
83 for pin in row.split(','): 255 return info_data
84 if pin == 'NO_PIN':
85 pin = None
86 256
87 direct_pin_array[-1].append(pin)
88 257
89 info_data['matrix_pins']['direct'] = direct_pin_array 258def _extract_config_h(info_data):
259 """Pull some keyboard information from existing config.h files
260 """
261 config_c = config_h(info_data['keyboard_folder'])
90 262
91 info_data['usb'] = { 263 # Pull in data from the json map
92 'vid': config_c.get('VENDOR_ID'), 264 dotty_info = dotty(info_data)
93 'pid': config_c.get('PRODUCT_ID'), 265 info_config_map = _json_load(Path('data/mappings/info_config.json'))
94 'device_ver': config_c.get('DEVICE_VER'), 266
95 'manufacturer': config_c.get('MANUFACTURER'), 267 for config_key, info_dict in info_config_map.items():
96 'product': config_c.get('PRODUCT'), 268 info_key = info_dict['info_key']
97 } 269 key_type = info_dict.get('value_type', 'str')
270
271 try:
272 if config_key in config_c and info_dict.get('to_json', True):
273 if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
274 _log_warning(info_data, '%s in config.h is overwriting %s in info.json' % (config_key, info_key))
275
276 if key_type.startswith('array'):
277 if '.' in key_type:
278 key_type, array_type = key_type.split('.', 1)
279 else:
280 array_type = None
281
282 config_value = config_c[config_key].replace('{', '').replace('}', '').strip()
283
284 if array_type == 'int':
285 dotty_info[info_key] = list(map(int, config_value.split(',')))
286 else:
287 dotty_info[info_key] = config_value.split(',')
288
289 elif key_type == 'bool':
290 dotty_info[info_key] = config_c[config_key] in true_values
291
292 elif key_type == 'hex':
293 dotty_info[info_key] = '0x' + config_c[config_key][2:].upper()
294
295 elif key_type == 'list':
296 dotty_info[info_key] = config_c[config_key].split()
297
298 elif key_type == 'int':
299 dotty_info[info_key] = int(config_c[config_key])
300
301 else:
302 dotty_info[info_key] = config_c[config_key]
303
304 except Exception as e:
305 _log_warning(info_data, f'{config_key}->{info_key}: {e}')
306
307 info_data.update(dotty_info)
308
309 # Pull data that easily can't be mapped in json
310 _extract_matrix_info(info_data, config_c)
98 311
99 return info_data 312 return info_data
100 313
@@ -103,19 +316,101 @@ def _extract_rules_mk(info_data):
103 """Pull some keyboard information from existing rules.mk files 316 """Pull some keyboard information from existing rules.mk files
104 """ 317 """
105 rules = rules_mk(info_data['keyboard_folder']) 318 rules = rules_mk(info_data['keyboard_folder'])
106 mcu = rules.get('MCU') 319 info_data['processor'] = rules.get('MCU', info_data.get('processor', 'atmega32u4'))
320
321 if info_data['processor'] in CHIBIOS_PROCESSORS:
322 arm_processor_rules(info_data, rules)
323
324 elif info_data['processor'] in LUFA_PROCESSORS + VUSB_PROCESSORS:
325 avr_processor_rules(info_data, rules)
326
327 else:
328 cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], info_data['processor']))
329 unknown_processor_rules(info_data, rules)
330
331 # Pull in data from the json map
332 dotty_info = dotty(info_data)
333 info_rules_map = _json_load(Path('data/mappings/info_rules.json'))
107 334
108 if mcu in CHIBIOS_PROCESSORS: 335 for rules_key, info_dict in info_rules_map.items():
109 return arm_processor_rules(info_data, rules) 336 info_key = info_dict['info_key']
337 key_type = info_dict.get('value_type', 'str')
110 338
111 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: 339 try:
112 return avr_processor_rules(info_data, rules) 340 if rules_key in rules and info_dict.get('to_json', True):
341 if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
342 _log_warning(info_data, '%s in rules.mk is overwriting %s in info.json' % (rules_key, info_key))
343
344 if key_type.startswith('array'):
345 if '.' in key_type:
346 key_type, array_type = key_type.split('.', 1)
347 else:
348 array_type = None
113 349
114 msg = "Unknown MCU: " + str(mcu) 350 rules_value = rules[rules_key].replace('{', '').replace('}', '').strip()
351
352 if array_type == 'int':
353 dotty_info[info_key] = list(map(int, rules_value.split(',')))
354 else:
355 dotty_info[info_key] = rules_value.split(',')
115 356
116 _log_warning(info_data, msg) 357 elif key_type == 'list':
358 dotty_info[info_key] = rules[rules_key].split()
117 359
118 return unknown_processor_rules(info_data, rules) 360 elif key_type == 'bool':
361 dotty_info[info_key] = rules[rules_key] in true_values
362
363 elif key_type == 'hex':
364 dotty_info[info_key] = '0x' + rules[rules_key][2:].upper()
365
366 elif key_type == 'int':
367 dotty_info[info_key] = int(rules[rules_key])
368
369 else:
370 dotty_info[info_key] = rules[rules_key]
371
372 except Exception as e:
373 _log_warning(info_data, f'{rules_key}->{info_key}: {e}')
374
375 info_data.update(dotty_info)
376
377 # Merge in config values that can't be easily mapped
378 _extract_features(info_data, rules)
379
380 return info_data
381
382
383def _merge_layouts(info_data, new_info_data):
384 """Merge new_info_data into info_data in an intelligent way.
385 """
386 for layout_name, layout_json in new_info_data['layouts'].items():
387 if layout_name in info_data['layouts']:
388 # Pull in layouts we have a macro for
389 if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
390 msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
391 _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
392 else:
393 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
394 key.update(layout_json['layout'][i])
395 else:
396 # Pull in layouts that have matrix data
397 missing_matrix = False
398 for key in layout_json.get('layout', {}):
399 if 'matrix' not in key:
400 missing_matrix = True
401
402 if not missing_matrix:
403 if layout_name in info_data['layouts']:
404 # Update an existing layout with new data
405 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
406 key.update(layout_json['layout'][i])
407
408 else:
409 # Copy in the new layout wholesale
410 layout_json['c_macro'] = False
411 info_data['layouts'][layout_name] = layout_json
412
413 return info_data
119 414
120 415
121def _search_keyboard_h(path): 416def _search_keyboard_h(path):
@@ -131,34 +426,21 @@ def _search_keyboard_h(path):
131 return layouts 426 return layouts
132 427
133 428
134def _find_all_layouts(info_data, keyboard, rules): 429def _find_all_layouts(info_data, keyboard):
135 """Looks for layout macros associated with this keyboard. 430 """Looks for layout macros associated with this keyboard.
136 """ 431 """
137 layouts = _search_keyboard_h(Path(keyboard)) 432 layouts = _search_keyboard_h(Path(keyboard))
138 433
139 if not layouts: 434 if not layouts:
140 # If we didn't find any layouts above we widen our search. This is error 435 # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
141 # prone which is why we want to encourage people to follow the standard above. 436 info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
142 _log_warning(info_data, 'Falling back to searching for KEYMAP/LAYOUT macros.') 437
143 for file in glob('keyboards/%s/*.h' % keyboard): 438 for file in glob('keyboards/%s/*.h' % keyboard):
144 if file.endswith('.h'): 439 if file.endswith('.h'):
145 these_layouts = find_layouts(file) 440 these_layouts = find_layouts(file)
146 if these_layouts: 441 if these_layouts:
147 layouts.update(these_layouts) 442 layouts.update(these_layouts)
148 443
149 if 'LAYOUTS' in rules:
150 # Match these up against the supplied layouts
151 supported_layouts = rules['LAYOUTS'].strip().split()
152 for layout_name in sorted(layouts):
153 if not layout_name.startswith('LAYOUT_'):
154 continue
155 layout_name = layout_name[7:]
156 if layout_name in supported_layouts:
157 supported_layouts.remove(layout_name)
158
159 if supported_layouts:
160 _log_error(info_data, 'Missing LAYOUT() macro for %s' % (', '.join(supported_layouts)))
161
162 return layouts 444 return layouts
163 445
164 446
@@ -180,13 +462,13 @@ def arm_processor_rules(info_data, rules):
180 """Setup the default info for an ARM board. 462 """Setup the default info for an ARM board.
181 """ 463 """
182 info_data['processor_type'] = 'arm' 464 info_data['processor_type'] = 'arm'
183 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
184 info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
185 info_data['protocol'] = 'ChibiOS' 465 info_data['protocol'] = 'ChibiOS'
186 466
187 if info_data['bootloader'] == 'unknown': 467 if 'bootloader' not in info_data:
188 if 'STM32' in info_data['processor']: 468 if 'STM32' in info_data['processor']:
189 info_data['bootloader'] = 'stm32-dfu' 469 info_data['bootloader'] = 'stm32-dfu'
470 else:
471 info_data['bootloader'] = 'unknown'
190 472
191 if 'STM32' in info_data['processor']: 473 if 'STM32' in info_data['processor']:
192 info_data['platform'] = 'STM32' 474 info_data['platform'] = 'STM32'
@@ -202,11 +484,12 @@ def avr_processor_rules(info_data, rules):
202 """Setup the default info for an AVR board. 484 """Setup the default info for an AVR board.
203 """ 485 """
204 info_data['processor_type'] = 'avr' 486 info_data['processor_type'] = 'avr'
205 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
206 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown' 487 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
207 info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
208 info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA' 488 info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
209 489
490 if 'bootloader' not in info_data:
491 info_data['bootloader'] = 'atmel-dfu'
492
210 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk: 493 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
211 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA' 494 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
212 495
@@ -225,38 +508,44 @@ def unknown_processor_rules(info_data, rules):
225 return info_data 508 return info_data
226 509
227 510
511def deep_update(origdict, newdict):
512 """Update a dictionary in place, recursing to do a deep copy.
513 """
514 for key, value in newdict.items():
515 if isinstance(value, Mapping):
516 origdict[key] = deep_update(origdict.get(key, {}), value)
517
518 else:
519 origdict[key] = value
520
521 return origdict
522
523
228def merge_info_jsons(keyboard, info_data): 524def merge_info_jsons(keyboard, info_data):
229 """Return a merged copy of all the info.json files for a keyboard. 525 """Return a merged copy of all the info.json files for a keyboard.
230 """ 526 """
231 for info_file in find_info_json(keyboard): 527 for info_file in find_info_json(keyboard):
232 # Load and validate the JSON data 528 # Load and validate the JSON data
233 try: 529 new_info_data = _json_load(info_file)
234 with info_file.open('r') as info_fd:
235 new_info_data = json.load(info_fd)
236 except Exception as e:
237 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
238 continue
239 530
240 if not isinstance(new_info_data, dict): 531 if not isinstance(new_info_data, dict):
241 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) 532 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
242 continue 533 continue
243 534
244 # Copy whitelisted keys into `info_data` 535 try:
245 for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): 536 keyboard_validate(new_info_data)
246 if key in new_info_data: 537 except jsonschema.ValidationError as e:
247 info_data[key] = new_info_data[key] 538 json_path = '.'.join([str(p) for p in e.absolute_path])
539 cli.log.error('Not including data from file: %s', info_file)
540 cli.log.error('\t%s: %s', json_path, e.message)
541 continue
542
543 # Mark the layouts as coming from json
544 for layout in new_info_data.get('layouts', {}).values():
545 layout['c_macro'] = False
248 546
249 # Merge the layouts in 547 # Update info_data with the new data
250 if 'layouts' in new_info_data: 548 deep_update(info_data, new_info_data)
251 for layout_name, json_layout in new_info_data['layouts'].items():
252 # Only pull in layouts we have a macro for
253 if layout_name in info_data['layouts']:
254 if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']):
255 msg = '%s: Number of elements in info.json does not match! info.json:%s != %s:%s'
256 _log_error(info_data, msg % (layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
257 else:
258 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
259 key.update(json_layout['layout'][i])
260 549
261 return info_data 550 return info_data
262 551
diff --git a/lib/python/qmk/info_json_encoder.py b/lib/python/qmk/info_json_encoder.py
new file mode 100755
index 000000000..60dae7247
--- /dev/null
+++ b/lib/python/qmk/info_json_encoder.py
@@ -0,0 +1,96 @@
1"""Class that pretty-prints QMK info.json files.
2"""
3import json
4from decimal import Decimal
5
6
7class InfoJSONEncoder(json.JSONEncoder):
8 """Custom encoder to make info.json's a little nicer to work with.
9 """
10 container_types = (list, tuple, dict)
11 indentation_char = " "
12
13 def __init__(self, *args, **kwargs):
14 super().__init__(*args, **kwargs)
15 self.indentation_level = 0
16
17 if not self.indent:
18 self.indent = 4
19
20 def encode(self, obj):
21 """Encode JSON objects for QMK.
22 """
23 if isinstance(obj, Decimal):
24 if obj == int(obj): # I can't believe Decimal objects don't have .is_integer()
25 return int(obj)
26 return float(obj)
27
28 elif isinstance(obj, (list, tuple)):
29 if self._primitives_only(obj):
30 return "[" + ", ".join(self.encode(element) for element in obj) + "]"
31
32 else:
33 self.indentation_level += 1
34 output = [self.indent_str + self.encode(element) for element in obj]
35 self.indentation_level -= 1
36 return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]"
37
38 elif isinstance(obj, dict):
39 if obj:
40 if self.indentation_level == 4:
41 # These are part of a layout, put them on a single line.
42 return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }"
43
44 else:
45 self.indentation_level += 1
46 output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_root_dict)]
47 self.indentation_level -= 1
48 return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"
49 else:
50 return "{}"
51 else:
52 return super().encode(obj)
53
54 def _primitives_only(self, obj):
55 """Returns true if the object doesn't have any container type objects (list, tuple, dict).
56 """
57 if isinstance(obj, dict):
58 obj = obj.values()
59
60 return not any(isinstance(element, self.container_types) for element in obj)
61
62 def sort_root_dict(self, key):
63 """Forces layout to the back of the sort order.
64 """
65 key = key[0]
66
67 if self.indentation_level == 1:
68 if key == 'manufacturer':
69 return '10keyboard_name'
70
71 elif key == 'keyboard_name':
72 return '11keyboard_name'
73
74 elif key == 'maintainer':
75 return '12maintainer'
76
77 elif key in ('height', 'width'):
78 return '40' + str(key)
79
80 elif key == 'community_layouts':
81 return '97community_layouts'
82
83 elif key == 'layout_aliases':
84 return '98layout_aliases'
85
86 elif key == 'layouts':
87 return '99layouts'
88
89 else:
90 return '50' + str(key)
91
92 return key
93
94 @property
95 def indent_str(self):
96 return self.indentation_char * (self.indentation_level * self.indent)
diff --git a/lib/python/qmk/os_helpers/linux/__init__.py b/lib/python/qmk/os_helpers/linux/__init__.py
index 86850bf28..a04ac4f8a 100644
--- a/lib/python/qmk/os_helpers/linux/__init__.py
+++ b/lib/python/qmk/os_helpers/linux/__init__.py
@@ -48,6 +48,7 @@ def check_udev_rules():
48 _udev_rule("03eb", "2ff3"), # ATmega16U4 48 _udev_rule("03eb", "2ff3"), # ATmega16U4
49 _udev_rule("03eb", "2ff4"), # ATmega32U4 49 _udev_rule("03eb", "2ff4"), # ATmega32U4
50 _udev_rule("03eb", "2ff9"), # AT90USB64 50 _udev_rule("03eb", "2ff9"), # AT90USB64
51 _udev_rule("03eb", "2ffa"), # AT90USB162
51 _udev_rule("03eb", "2ffb") # AT90USB128 52 _udev_rule("03eb", "2ffb") # AT90USB128
52 }, 53 },
53 'kiibohd': {_udev_rule("1c11", "b007")}, 54 'kiibohd': {_udev_rule("1c11", "b007")},
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index f889833d0..3efeddb85 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -230,3 +230,32 @@ def test_generate_rgb_breathe_table():
230 check_returncode(result) 230 check_returncode(result)
231 assert 'Breathing center: 1.2' in result.stdout 231 assert 'Breathing center: 1.2' in result.stdout
232 assert 'Breathing max: 127' in result.stdout 232 assert 'Breathing max: 127' in result.stdout
233
234
235def test_generate_config_h():
236 result = check_subcommand('generate-config-h', '-kb', 'handwired/pytest/basic')
237 check_returncode(result)
238 assert '# define DEVICE_VER 0x0001' in result.stdout
239 assert '# define DESCRIPTION handwired/pytest/basic' in result.stdout
240 assert '# define DIODE_DIRECTION COL2ROW' in result.stdout
241 assert '# define MANUFACTURER none' in result.stdout
242 assert '# define PRODUCT handwired/pytest/basic' in result.stdout
243 assert '# define PRODUCT_ID 0x6465' in result.stdout
244 assert '# define VENDOR_ID 0xFEED' in result.stdout
245 assert '# define MATRIX_COLS 1' in result.stdout
246 assert '# define MATRIX_COL_PINS { F4 }' in result.stdout
247 assert '# define MATRIX_ROWS 1' in result.stdout
248 assert '# define MATRIX_ROW_PINS { F5 }' in result.stdout
249
250
251def test_generate_rules_mk():
252 result = check_subcommand('generate-rules-mk', '-kb', 'handwired/pytest/basic')
253 check_returncode(result)
254 assert 'BOOTLOADER ?= atmel-dfu' in result.stdout
255 assert 'MCU ?= atmega32u4' in result.stdout
256
257
258def test_generate_layouts():
259 result = check_subcommand('generate-layouts', '-kb', 'handwired/pytest/basic')
260 check_returncode(result)
261 assert '#define LAYOUT_custom(k0A) {' in result.stdout