aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/c_parse.py26
-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.py277
-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.py78
-rwxr-xr-xlib/python/qmk/cli/info.py7
-rwxr-xr-xlib/python/qmk/cli/kle2json.py51
-rw-r--r--lib/python/qmk/constants.py13
-rw-r--r--lib/python/qmk/info.py592
-rwxr-xr-xlib/python/qmk/info_json_encoder.py96
-rw-r--r--lib/python/qmk/os_helpers/linux/__init__.py1
15 files changed, 1197 insertions, 137 deletions
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index e41e271a4..ade3e3805 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):
@@ -53,7 +68,8 @@ def find_layouts(file):
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 key in parsed_layout:
56 key['matrix'] = matrix_locations.get(key['label']) 71 if key['label'] in matrix_locations:
72 key['matrix'] = matrix_locations[key['label']]
57 73
58 parsed_layouts[macro_name] = { 74 parsed_layouts[macro_name] = {
59 'key_count': len(parsed_layout), 75 'key_count': len(parsed_layout),
@@ -88,12 +104,10 @@ def parse_config_h_file(config_h_file, config_h=None):
88 if config_h_file.exists(): 104 if config_h_file.exists():
89 config_h_text = config_h_file.read_text() 105 config_h_text = config_h_file.read_text()
90 config_h_text = config_h_text.replace('\\\n', '') 106 config_h_text = config_h_text.replace('\\\n', '')
107 config_h_text = strip_multiline_comment(config_h_text)
91 108
92 for linenum, line in enumerate(config_h_text.split('\n')): 109 for linenum, line in enumerate(config_h_text.split('\n')):
93 line = line.strip() 110 line = strip_line_comment(line).strip()
94
95 if '//' in line:
96 line = line[:line.index('//')].strip()
97 111
98 if not line: 112 if not line:
99 continue 113 continue
@@ -156,6 +170,6 @@ def _parse_matrix_locations(matrix, file, macro_name):
156 row = row.replace('{', '').replace('}', '') 170 row = row.replace('{', '').replace('}', '')
157 for col_num, identifier in enumerate(row.split(',')): 171 for col_num, identifier in enumerate(row.split(',')):
158 if identifier != 'KC_NO': 172 if identifier != 'KC_NO':
159 matrix_locations[identifier] = (row_num, col_num) 173 matrix_locations[identifier] = [row_num, col_num]
160 174
161 return matrix_locations 175 return matrix_locations
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..1de84de7a
--- /dev/null
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -0,0 +1,277 @@
1"""Used by the make system to generate info_config.h from info.json.
2"""
3from milc import cli
4
5from qmk.constants import LED_INDICATORS
6from qmk.decorators import automagic_keyboard, automagic_keymap
7from qmk.info import info_json, rgblight_animations, rgblight_properties, rgblight_toggles
8from qmk.path import is_keyboard, normpath
9
10usb_prop_map = {
11 'vid': 'VENDOR_ID',
12 'pid': 'PRODUCT_ID',
13 'device_ver': 'DEVICE_VER',
14}
15
16
17def debounce(debounce):
18 """Return the config.h lines that set debounce
19 """
20 return """
21#ifndef DEBOUNCE
22# define DEBOUNCE %s
23#endif // DEBOUNCE
24""" % debounce
25
26
27def diode_direction(diode_direction):
28 """Return the config.h lines that set diode direction
29 """
30 return """
31#ifndef DIODE_DIRECTION
32# define DIODE_DIRECTION %s
33#endif // DIODE_DIRECTION
34""" % diode_direction
35
36
37def keyboard_name(keyboard_name):
38 """Return the config.h lines that set the keyboard's name.
39 """
40 return """
41#ifndef DESCRIPTION
42# define DESCRIPTION %s
43#endif // DESCRIPTION
44
45#ifndef PRODUCT
46# define PRODUCT %s
47#endif // PRODUCT
48""" % (keyboard_name.replace("'", ""), keyboard_name.replace("'", ""))
49
50
51def manufacturer(manufacturer):
52 """Return the config.h lines that set the manufacturer.
53 """
54 return """
55#ifndef MANUFACTURER
56# define MANUFACTURER %s
57#endif // MANUFACTURER
58""" % (manufacturer.replace("'", ""))
59
60
61def direct_pins(direct_pins):
62 """Return the config.h lines that set the direct pins.
63 """
64 rows = []
65
66 for row in direct_pins:
67 cols = ','.join(map(str, [col or 'NO_PIN' for col in row]))
68 rows.append('{' + cols + '}')
69
70 col_count = len(direct_pins[0])
71 row_count = len(direct_pins)
72
73 return """
74#ifndef MATRIX_COLS
75# define MATRIX_COLS %s
76#endif // MATRIX_COLS
77
78#ifndef MATRIX_ROWS
79# define MATRIX_ROWS %s
80#endif // MATRIX_ROWS
81
82#ifndef DIRECT_PINS
83# define DIRECT_PINS {%s}
84#endif // DIRECT_PINS
85""" % (col_count, row_count, ','.join(rows))
86
87
88def col_pins(col_pins):
89 """Return the config.h lines that set the column pins.
90 """
91 cols = ','.join(map(str, [pin or 'NO_PIN' for pin in col_pins]))
92 col_num = len(col_pins)
93
94 return """
95#ifndef MATRIX_COLS
96# define MATRIX_COLS %s
97#endif // MATRIX_COLS
98
99#ifndef MATRIX_COL_PINS
100# define MATRIX_COL_PINS {%s}
101#endif // MATRIX_COL_PINS
102""" % (col_num, cols)
103
104
105def row_pins(row_pins):
106 """Return the config.h lines that set the row pins.
107 """
108 rows = ','.join(map(str, [pin or 'NO_PIN' for pin in row_pins]))
109 row_num = len(row_pins)
110
111 return """
112#ifndef MATRIX_ROWS
113# define MATRIX_ROWS %s
114#endif // MATRIX_ROWS
115
116#ifndef MATRIX_ROW_PINS
117# define MATRIX_ROW_PINS {%s}
118#endif // MATRIX_ROW_PINS
119""" % (row_num, rows)
120
121
122def indicators(config):
123 """Return the config.h lines that setup LED indicators.
124 """
125 defines = []
126
127 for led, define in LED_INDICATORS.items():
128 if led in config:
129 defines.append('')
130 defines.append('#ifndef %s' % (define,))
131 defines.append('# define %s %s' % (define, config[led]))
132 defines.append('#endif // %s' % (define,))
133
134 return '\n'.join(defines)
135
136
137def layout_aliases(layout_aliases):
138 """Return the config.h lines that setup layout aliases.
139 """
140 defines = []
141
142 for alias, layout in layout_aliases.items():
143 defines.append('')
144 defines.append('#ifndef %s' % (alias,))
145 defines.append('# define %s %s' % (alias, layout))
146 defines.append('#endif // %s' % (alias,))
147
148 return '\n'.join(defines)
149
150
151def matrix_pins(matrix_pins):
152 """Add the matrix config to the config.h.
153 """
154 pins = []
155
156 if 'direct' in matrix_pins:
157 pins.append(direct_pins(matrix_pins['direct']))
158
159 if 'cols' in matrix_pins:
160 pins.append(col_pins(matrix_pins['cols']))
161
162 if 'rows' in matrix_pins:
163 pins.append(row_pins(matrix_pins['rows']))
164
165 return '\n'.join(pins)
166
167
168def rgblight(config):
169 """Return the config.h lines that setup rgblight.
170 """
171 rgblight_config = []
172
173 for json_key, config_key in rgblight_properties.items():
174 if json_key in config:
175 rgblight_config.append('')
176 rgblight_config.append('#ifndef %s' % (config_key[0],))
177 rgblight_config.append('# define %s %s' % (config_key[0], config[json_key]))
178 rgblight_config.append('#endif // %s' % (config_key[0],))
179
180 for json_key, config_key in rgblight_toggles.items():
181 if config.get(json_key):
182 rgblight_config.append('')
183 rgblight_config.append('#ifndef %s' % (config_key,))
184 rgblight_config.append('# define %s' % (config_key,))
185 rgblight_config.append('#endif // %s' % (config_key,))
186
187 for json_key, config_key in rgblight_animations.items():
188 if 'animations' in config and config['animations'].get(json_key):
189 rgblight_config.append('')
190 rgblight_config.append('#ifndef %s' % (config_key,))
191 rgblight_config.append('# define %s' % (config_key,))
192 rgblight_config.append('#endif // %s' % (config_key,))
193
194 return '\n'.join(rgblight_config)
195
196
197def usb_properties(usb_props):
198 """Return the config.h lines that setup USB params.
199 """
200 usb_lines = []
201
202 for info_name, config_name in usb_prop_map.items():
203 if info_name in usb_props:
204 usb_lines.append('')
205 usb_lines.append('#ifndef ' + config_name)
206 usb_lines.append('# define %s %s' % (config_name, usb_props[info_name]))
207 usb_lines.append('#endif // ' + config_name)
208
209 return '\n'.join(usb_lines)
210
211
212@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
213@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
214@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
215@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
216@automagic_keyboard
217@automagic_keymap
218def generate_config_h(cli):
219 """Generates the info_config.h file.
220 """
221 # Determine our keyboard(s)
222 if not cli.config.generate_config_h.keyboard:
223 cli.log.error('Missing paramater: --keyboard')
224 cli.subcommands['info'].print_help()
225 return False
226
227 if not is_keyboard(cli.config.generate_config_h.keyboard):
228 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
229 return False
230
231 # Build the info.json file
232 kb_info_json = info_json(cli.config.generate_config_h.keyboard)
233
234 # Build the info_config.h file.
235 config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
236
237 if 'debounce' in kb_info_json:
238 config_h_lines.append(debounce(kb_info_json['debounce']))
239
240 if 'diode_direction' in kb_info_json:
241 config_h_lines.append(diode_direction(kb_info_json['diode_direction']))
242
243 if 'indicators' in kb_info_json:
244 config_h_lines.append(indicators(kb_info_json['indicators']))
245
246 if 'keyboard_name' in kb_info_json:
247 config_h_lines.append(keyboard_name(kb_info_json['keyboard_name']))
248
249 if 'layout_aliases' in kb_info_json:
250 config_h_lines.append(layout_aliases(kb_info_json['layout_aliases']))
251
252 if 'manufacturer' in kb_info_json:
253 config_h_lines.append(manufacturer(kb_info_json['manufacturer']))
254
255 if 'rgblight' in kb_info_json:
256 config_h_lines.append(rgblight(kb_info_json['rgblight']))
257
258 if 'matrix_pins' in kb_info_json:
259 config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
260
261 if 'usb' in kb_info_json:
262 config_h_lines.append(usb_properties(kb_info_json['usb']))
263
264 # Show the results
265 config_h = '\n'.join(config_h_lines)
266
267 if cli.args.output:
268 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
269 if cli.args.output.exists():
270 cli.args.output.replace(cli.args.output.name + '.bak')
271 cli.args.output.write_text(config_h)
272
273 if not cli.args.quiet:
274 cli.log.info('Wrote info_config.h to %s.', cli.args.output)
275
276 else:
277 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..b262e3c66
--- /dev/null
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -0,0 +1,78 @@
1"""Used by the make system to generate a rules.mk
2"""
3from milc import cli
4
5from qmk.decorators import automagic_keyboard, automagic_keymap
6from qmk.info import info_json
7from qmk.path import is_keyboard, normpath
8
9info_to_rules = {
10 'board': 'BOARD',
11 'bootloader': 'BOOTLOADER',
12 'processor': 'MCU',
13}
14
15
16@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
17@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
18@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
19@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
20@automagic_keyboard
21@automagic_keymap
22def generate_rules_mk(cli):
23 """Generates a rules.mk file from info.json.
24 """
25 # Determine our keyboard(s)
26 if not cli.config.generate_rules_mk.keyboard:
27 cli.log.error('Missing paramater: --keyboard')
28 cli.subcommands['info'].print_help()
29 return False
30
31 if not is_keyboard(cli.config.generate_rules_mk.keyboard):
32 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard)
33 return False
34
35 # Build the info.json file
36 kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
37 rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
38
39 # Bring in settings
40 for info_key, rule_key in info_to_rules.items():
41 if info_key in kb_info_json:
42 rules_mk_lines.append(f'{rule_key} ?= {kb_info_json[info_key]}')
43
44 # Find features that should be enabled
45 if 'features' in kb_info_json:
46 for feature, enabled in kb_info_json['features'].items():
47 if feature == 'bootmagic_lite' and enabled:
48 rules_mk_lines.append('BOOTMAGIC_ENABLE ?= lite')
49 else:
50 feature = feature.upper()
51 enabled = 'yes' if enabled else 'no'
52 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
53
54 # Set the LED driver
55 if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
56 driver = kb_info_json['led_matrix']['driver']
57 rules_mk_lines.append(f'LED_MATRIX_DRIVER ?= {driver}')
58
59 # Add community layouts
60 if 'community_layouts' in kb_info_json:
61 rules_mk_lines.append(f'LAYOUTS ?= {" ".join(kb_info_json["community_layouts"])}')
62
63 # Show the results
64 rules_mk = '\n'.join(rules_mk_lines) + '\n'
65
66 if cli.args.output:
67 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
68 if cli.args.output.exists():
69 cli.args.output.replace(cli.args.output.name + '.bak')
70 cli.args.output.write_text(rules_mk)
71
72 if cli.args.quiet:
73 print(cli.args.output)
74 else:
75 cli.log.info('Wrote info_config.h to %s.', cli.args.output)
76
77 else:
78 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/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..cb9461356 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -11,7 +11,7 @@ MAX_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'
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 'scrol_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..cc81f7a08 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -1,18 +1,64 @@
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 jsonschema
7from milc import cli 9from milc import cli
8 10
9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 11from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
10from qmk.c_parse import find_layouts 12from qmk.c_parse import find_layouts
11from qmk.keyboard import config_h, rules_mk 13from qmk.keyboard import config_h, rules_mk
12from qmk.keymap import list_keymaps 14from qmk.keymap import list_keymaps
13from qmk.makefile import parse_rules_mk_file 15from qmk.makefile import parse_rules_mk_file
14from qmk.math import compute 16from qmk.math import compute
15 17
18led_matrix_properties = {
19 'driver_count': 'LED_DRIVER_COUNT',
20 'driver_addr1': 'LED_DRIVER_ADDR_1',
21 'driver_addr2': 'LED_DRIVER_ADDR_2',
22 'driver_addr3': 'LED_DRIVER_ADDR_3',
23 'driver_addr4': 'LED_DRIVER_ADDR_4',
24 'led_count': 'LED_DRIVER_LED_COUNT',
25 'timeout': 'ISSI_TIMEOUT',
26 'persistence': 'ISSI_PERSISTENCE'
27}
28
29rgblight_properties = {
30 'led_count': ('RGBLED_NUM', int),
31 'pin': ('RGB_DI_PIN', str),
32 'max_brightness': ('RGBLIGHT_LIMIT_VAL', int),
33 'hue_steps': ('RGBLIGHT_HUE_STEP', int),
34 'saturation_steps': ('RGBLIGHT_SAT_STEP', int),
35 'brightness_steps': ('RGBLIGHT_VAL_STEP', int)
36}
37
38rgblight_toggles = {
39 'sleep': 'RGBLIGHT_SLEEP',
40 'split': 'RGBLIGHT_SPLIT',
41}
42
43rgblight_animations = {
44 'all': 'RGBLIGHT_ANIMATIONS',
45 'alternating': 'RGBLIGHT_EFFECT_ALTERNATING',
46 'breathing': 'RGBLIGHT_EFFECT_BREATHING',
47 'christmas': 'RGBLIGHT_EFFECT_CHRISTMAS',
48 'knight': 'RGBLIGHT_EFFECT_KNIGHT',
49 'rainbow_mood': 'RGBLIGHT_EFFECT_RAINBOW_MOOD',
50 'rainbow_swirl': 'RGBLIGHT_EFFECT_RAINBOW_SWIRL',
51 'rgb_test': 'RGBLIGHT_EFFECT_RGB_TEST',
52 'snake': 'RGBLIGHT_EFFECT_SNAKE',
53 'static_gradient': 'RGBLIGHT_EFFECT_STATIC_GRADIENT',
54 'twinkle': 'RGBLIGHT_EFFECT_TWINKLE'
55}
56
57usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'}
58
59true_values = ['1', 'on', 'yes']
60false_values = ['0', 'off', 'no']
61
16 62
17def info_json(keyboard): 63def info_json(keyboard):
18 """Generate the info.json data for a specific keyboard. 64 """Generate the info.json data for a specific keyboard.
@@ -38,8 +84,9 @@ def info_json(keyboard):
38 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} 84 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
39 85
40 # Populate layout data 86 # Populate layout data
41 for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items(): 87 for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items():
42 if not layout_name.startswith('LAYOUT_kc'): 88 if not layout_name.startswith('LAYOUT_kc'):
89 layout_json['c_macro'] = True
43 info_data['layouts'][layout_name] = layout_json 90 info_data['layouts'][layout_name] = layout_json
44 91
45 # Merge in the data from info.json, config.h, and rules.mk 92 # Merge in the data from info.json, config.h, and rules.mk
@@ -47,54 +94,366 @@ def info_json(keyboard):
47 info_data = _extract_config_h(info_data) 94 info_data = _extract_config_h(info_data)
48 info_data = _extract_rules_mk(info_data) 95 info_data = _extract_rules_mk(info_data)
49 96
97 # Validate against the jsonschema
98 try:
99 keyboard_api_validate(info_data)
100
101 except jsonschema.ValidationError as e:
102 json_path = '.'.join([str(p) for p in e.absolute_path])
103 cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
104 print(dir(e))
105 exit()
106
107 # Make sure we have at least one layout
108 if not info_data.get('layouts'):
109 _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
110
111 # Make sure we supply layout macros for the community layouts we claim to support
112 # FIXME(skullydazed): This should be populated into info.json and read from there instead
113 if 'LAYOUTS' in rules and info_data.get('layouts'):
114 # Match these up against the supplied layouts
115 supported_layouts = rules['LAYOUTS'].strip().split()
116 for layout_name in sorted(info_data['layouts']):
117 layout_name = layout_name[7:]
118
119 if layout_name in supported_layouts:
120 supported_layouts.remove(layout_name)
121
122 if supported_layouts:
123 for supported_layout in supported_layouts:
124 _log_error(info_data, 'Claims to support community layout %s but no LAYOUT_%s() macro found' % (supported_layout, supported_layout))
125
50 return info_data 126 return info_data
51 127
52 128
53def _extract_config_h(info_data): 129def _json_load(json_file):
54 """Pull some keyboard information from existing rules.mk files 130 """Load a json file from disk.
131
132 Note: file must be a Path object.
133 """
134 try:
135 return json.load(json_file.open())
136
137 except json.decoder.JSONDecodeError as e:
138 cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
139 exit(1)
140
141
142def _jsonschema(schema_name):
143 """Read a jsonschema file from disk.
144
145 FIXME(skullydazed/anyone): Refactor to make this a public function.
146 """
147 schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
148
149 if not schema_path.exists():
150 schema_path = Path('data/schemas/false.jsonschema')
151
152 return _json_load(schema_path)
153
154
155def keyboard_validate(data):
156 """Validates data against the keyboard jsonschema.
157 """
158 schema = _jsonschema('keyboard')
159 validator = jsonschema.Draft7Validator(schema).validate
160
161 return validator(data)
162
163
164def keyboard_api_validate(data):
165 """Validates data against the api_keyboard jsonschema.
166 """
167 base = _jsonschema('keyboard')
168 relative = _jsonschema('api_keyboard')
169 resolver = jsonschema.RefResolver.from_schema(base)
170 validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
171
172 return validator(data)
173
174
175def _extract_debounce(info_data, config_c):
176 """Handle debounce.
177 """
178 if 'debounce' in info_data and 'DEBOUNCE' in config_c:
179 _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
180
181 if 'DEBOUNCE' in config_c:
182 info_data['debounce'] = int(config_c['DEBOUNCE'])
183
184 return info_data
185
186
187def _extract_diode_direction(info_data, config_c):
188 """Handle the diode direction.
189 """
190 if 'diode_direction' in info_data and 'DIODE_DIRECTION' in config_c:
191 _log_warning(info_data, 'Diode direction is specified in both info.json and config.h, the config.h value wins.')
192
193 if 'DIODE_DIRECTION' in config_c:
194 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION')
195
196 return info_data
197
198
199def _extract_indicators(info_data, config_c):
200 """Find the LED indicator information.
201 """
202 for json_key, config_key in LED_INDICATORS.items():
203 if json_key in info_data.get('indicators', []) and config_key in config_c:
204 _log_warning(info_data, f'Indicator {json_key} is specified in both info.json and config.h, the config.h value wins.')
205
206 if 'indicators' not in info_data:
207 info_data['indicators'] = {}
208
209 if config_key in config_c:
210 if 'indicators' not in info_data:
211 info_data['indicators'] = {}
212
213 info_data['indicators'][json_key] = config_c.get(config_key)
214
215 return info_data
216
217
218def _extract_community_layouts(info_data, rules):
219 """Find the community layouts in rules.mk.
220 """
221 community_layouts = rules['LAYOUTS'].split() if 'LAYOUTS' in rules else []
222
223 if 'community_layouts' in info_data:
224 for layout in community_layouts:
225 if layout not in info_data['community_layouts']:
226 community_layouts.append(layout)
227
228 else:
229 info_data['community_layouts'] = community_layouts
230
231 return info_data
232
233
234def _extract_features(info_data, rules):
235 """Find all the features enabled in rules.mk.
236 """
237 # Special handling for bootmagic which also supports a "lite" mode.
238 if rules.get('BOOTMAGIC_ENABLE') == 'lite':
239 rules['BOOTMAGIC_LITE_ENABLE'] = 'on'
240 del rules['BOOTMAGIC_ENABLE']
241 if rules.get('BOOTMAGIC_ENABLE') == 'full':
242 rules['BOOTMAGIC_ENABLE'] = 'on'
243
244 # Skip non-boolean features we haven't implemented special handling for
245 for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
246 if rules.get(feature):
247 del rules[feature]
248
249 # Process the rest of the rules as booleans
250 for key, value in rules.items():
251 if key.endswith('_ENABLE'):
252 key = '_'.join(key.split('_')[:-1]).lower()
253 value = True if value.lower() in true_values else False if value.lower() in false_values else value
254
255 if 'config_h_features' not in info_data:
256 info_data['config_h_features'] = {}
257
258 if 'features' not in info_data:
259 info_data['features'] = {}
260
261 if key in info_data['features']:
262 _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
263
264 info_data['features'][key] = value
265 info_data['config_h_features'][key] = value
266
267 return info_data
268
269
270def _extract_led_drivers(info_data, rules):
271 """Find all the LED drivers set in rules.mk.
272 """
273 if 'LED_MATRIX_DRIVER' in rules:
274 if 'led_matrix' not in info_data:
275 info_data['led_matrix'] = {}
276
277 if info_data['led_matrix'].get('driver'):
278 _log_warning(info_data, 'LED Matrix driver is specified in both info.json and rules.mk, the rules.mk value wins.')
279
280 info_data['led_matrix']['driver'] = rules['LED_MATRIX_DRIVER']
281
282 return info_data
283
284
285def _extract_led_matrix(info_data, config_c):
286 """Handle the led_matrix configuration.
287 """
288 led_matrix = info_data.get('led_matrix', {})
289
290 for json_key, config_key in led_matrix_properties.items():
291 if config_key in config_c:
292 if json_key in led_matrix:
293 _log_warning(info_data, 'LED Matrix: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
294
295 led_matrix[json_key] = config_c[config_key]
296
297
298def _extract_rgblight(info_data, config_c):
299 """Handle the rgblight configuration.
300 """
301 rgblight = info_data.get('rgblight', {})
302 animations = rgblight.get('animations', {})
303
304 if 'RGBLED_SPLIT' in config_c:
305 raw_split = config_c.get('RGBLED_SPLIT', '').replace('{', '').replace('}', '').strip()
306 rgblight['split_count'] = [int(i) for i in raw_split.split(',')]
307
308 for json_key, config_key_type in rgblight_properties.items():
309 config_key, config_type = config_key_type
310
311 if config_key in config_c:
312 if json_key in rgblight:
313 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
314
315 try:
316 rgblight[json_key] = config_type(config_c[config_key])
317 except ValueError as e:
318 cli.log.error('%s: config.h: Could not convert "%s" to %s: %s', info_data['keyboard_folder'], config_c[config_key], config_type.__name__, e)
319
320 for json_key, config_key in rgblight_toggles.items():
321 if config_key in config_c and json_key in rgblight:
322 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key)
323
324 rgblight[json_key] = config_key in config_c
325
326 for json_key, config_key in rgblight_animations.items():
327 if config_key in config_c:
328 if json_key in animations:
329 _log_warning(info_data, 'RGB Light: animations: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
330
331 animations[json_key] = config_c[config_key]
332
333 if animations:
334 rgblight['animations'] = animations
335
336 if rgblight:
337 info_data['rgblight'] = rgblight
338
339 return info_data
340
341
342def _pin_name(pin):
343 """Returns the proper representation for a pin.
344 """
345 pin = pin.strip()
346
347 if not pin:
348 return None
349
350 elif pin.isdigit():
351 return int(pin)
352
353 elif pin == 'NO_PIN':
354 return None
355
356 elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit():
357 return pin
358
359 raise ValueError(f'Invalid pin: {pin}')
360
361
362def _extract_pins(pins):
363 """Returns a list of pins from a comma separated string of pins.
364 """
365 return [_pin_name(pin) for pin in pins.split(',')]
366
367
368def _extract_direct_matrix(info_data, direct_pins):
369 """
370 """
371 info_data['matrix_pins'] = {}
372 direct_pin_array = []
373
374 while direct_pins[-1] != '}':
375 direct_pins = direct_pins[:-1]
376
377 for row in direct_pins.split('},{'):
378 if row.startswith('{'):
379 row = row[1:]
380
381 if row.endswith('}'):
382 row = row[:-1]
383
384 direct_pin_array.append([])
385
386 for pin in row.split(','):
387 if pin == 'NO_PIN':
388 pin = None
389
390 direct_pin_array[-1].append(pin)
391
392 return direct_pin_array
393
394
395def _extract_matrix_info(info_data, config_c):
396 """Populate the matrix information.
55 """ 397 """
56 config_c = config_h(info_data['keyboard_folder'])
57 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() 398 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
58 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() 399 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
59 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] 400 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
60 401
61 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION') 402 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
62 info_data['matrix_size'] = { 403 if 'matrix_size' in info_data:
63 'rows': compute(config_c.get('MATRIX_ROWS', '0')), 404 _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')), 405
65 } 406 info_data['matrix_size'] = {
66 info_data['matrix_pins'] = {} 407 'cols': compute(config_c.get('MATRIX_COLS', '0')),
408 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
409 }
410
411 if row_pins and col_pins:
412 if 'matrix_pins' in info_data:
413 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
67 414
68 if row_pins: 415 info_data['matrix_pins'] = {
69 info_data['matrix_pins']['rows'] = row_pins.split(',') 416 'cols': _extract_pins(col_pins),
70 if col_pins: 417 'rows': _extract_pins(row_pins),
71 info_data['matrix_pins']['cols'] = col_pins.split(',') 418 }
72 419
73 if direct_pins: 420 if direct_pins:
74 direct_pin_array = [] 421 if 'matrix_pins' in info_data:
75 for row in direct_pins.split('},{'): 422 _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 423
81 direct_pin_array.append([]) 424 info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
82 425
83 for pin in row.split(','): 426 return info_data
84 if pin == 'NO_PIN':
85 pin = None
86 427
87 direct_pin_array[-1].append(pin)
88 428
89 info_data['matrix_pins']['direct'] = direct_pin_array 429def _extract_usb_info(info_data, config_c):
430 """Populate the USB information.
431 """
432 if 'usb' not in info_data:
433 info_data['usb'] = {}
90 434
91 info_data['usb'] = { 435 for info_name, config_name in usb_properties.items():
92 'vid': config_c.get('VENDOR_ID'), 436 if config_name in config_c:
93 'pid': config_c.get('PRODUCT_ID'), 437 if info_name in info_data['usb']:
94 'device_ver': config_c.get('DEVICE_VER'), 438 _log_warning(info_data, '%s in config.h is overwriting usb.%s in info.json' % (config_name, info_name))
95 'manufacturer': config_c.get('MANUFACTURER'), 439
96 'product': config_c.get('PRODUCT'), 440 info_data['usb'][info_name] = '0x' + config_c[config_name][2:].upper()
97 } 441
442 return info_data
443
444
445def _extract_config_h(info_data):
446 """Pull some keyboard information from existing config.h files
447 """
448 config_c = config_h(info_data['keyboard_folder'])
449
450 _extract_debounce(info_data, config_c)
451 _extract_diode_direction(info_data, config_c)
452 _extract_indicators(info_data, config_c)
453 _extract_matrix_info(info_data, config_c)
454 _extract_usb_info(info_data, config_c)
455 _extract_led_matrix(info_data, config_c)
456 _extract_rgblight(info_data, config_c)
98 457
99 return info_data 458 return info_data
100 459
@@ -103,19 +462,56 @@ def _extract_rules_mk(info_data):
103 """Pull some keyboard information from existing rules.mk files 462 """Pull some keyboard information from existing rules.mk files
104 """ 463 """
105 rules = rules_mk(info_data['keyboard_folder']) 464 rules = rules_mk(info_data['keyboard_folder'])
106 mcu = rules.get('MCU') 465 mcu = rules.get('MCU', info_data.get('processor'))
107 466
108 if mcu in CHIBIOS_PROCESSORS: 467 if mcu in CHIBIOS_PROCESSORS:
109 return arm_processor_rules(info_data, rules) 468 arm_processor_rules(info_data, rules)
110 469
111 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: 470 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
112 return avr_processor_rules(info_data, rules) 471 avr_processor_rules(info_data, rules)
472
473 else:
474 cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu))
475 unknown_processor_rules(info_data, rules)
476
477 _extract_community_layouts(info_data, rules)
478 _extract_features(info_data, rules)
479 _extract_led_drivers(info_data, rules)
113 480
114 msg = "Unknown MCU: " + str(mcu) 481 return info_data
482
483
484def _merge_layouts(info_data, new_info_data):
485 """Merge new_info_data into info_data in an intelligent way.
486 """
487 for layout_name, layout_json in new_info_data['layouts'].items():
488 if layout_name in info_data['layouts']:
489 # Pull in layouts we have a macro for
490 if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
491 msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
492 _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
493 else:
494 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
495 key.update(layout_json['layout'][i])
496 else:
497 # Pull in layouts that have matrix data
498 missing_matrix = False
499 for key in layout_json.get('layout', {}):
500 if 'matrix' not in key:
501 missing_matrix = True
502
503 if not missing_matrix:
504 if layout_name in info_data['layouts']:
505 # Update an existing layout with new data
506 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
507 key.update(layout_json['layout'][i])
115 508
116 _log_warning(info_data, msg) 509 else:
510 # Copy in the new layout wholesale
511 layout_json['c_macro'] = False
512 info_data['layouts'][layout_name] = layout_json
117 513
118 return unknown_processor_rules(info_data, rules) 514 return info_data
119 515
120 516
121def _search_keyboard_h(path): 517def _search_keyboard_h(path):
@@ -131,34 +527,21 @@ def _search_keyboard_h(path):
131 return layouts 527 return layouts
132 528
133 529
134def _find_all_layouts(info_data, keyboard, rules): 530def _find_all_layouts(info_data, keyboard):
135 """Looks for layout macros associated with this keyboard. 531 """Looks for layout macros associated with this keyboard.
136 """ 532 """
137 layouts = _search_keyboard_h(Path(keyboard)) 533 layouts = _search_keyboard_h(Path(keyboard))
138 534
139 if not layouts: 535 if not layouts:
140 # If we didn't find any layouts above we widen our search. This is error 536 # 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. 537 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.') 538
143 for file in glob('keyboards/%s/*.h' % keyboard): 539 for file in glob('keyboards/%s/*.h' % keyboard):
144 if file.endswith('.h'): 540 if file.endswith('.h'):
145 these_layouts = find_layouts(file) 541 these_layouts = find_layouts(file)
146 if these_layouts: 542 if these_layouts:
147 layouts.update(these_layouts) 543 layouts.update(these_layouts)
148 544
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 545 return layouts
163 546
164 547
@@ -180,13 +563,29 @@ def arm_processor_rules(info_data, rules):
180 """Setup the default info for an ARM board. 563 """Setup the default info for an ARM board.
181 """ 564 """
182 info_data['processor_type'] = 'arm' 565 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' 566 info_data['protocol'] = 'ChibiOS'
186 567
187 if info_data['bootloader'] == 'unknown': 568 if 'MCU' in rules:
569 if 'processor' in info_data:
570 _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
571
572 info_data['processor'] = rules['MCU']
573
574 elif 'processor' not in info_data:
575 info_data['processor'] = 'unknown'
576
577 if 'BOOTLOADER' in rules:
578 # FIXME(skullydazed/anyone): need to remove the massive amounts of duplication first
579 # if 'bootloader' in info_data:
580 # _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
581
582 info_data['bootloader'] = rules['BOOTLOADER']
583
584 else:
188 if 'STM32' in info_data['processor']: 585 if 'STM32' in info_data['processor']:
189 info_data['bootloader'] = 'stm32-dfu' 586 info_data['bootloader'] = 'stm32-dfu'
587 else:
588 info_data['bootloader'] = 'unknown'
190 589
191 if 'STM32' in info_data['processor']: 590 if 'STM32' in info_data['processor']:
192 info_data['platform'] = 'STM32' 591 info_data['platform'] = 'STM32'
@@ -195,6 +594,12 @@ def arm_processor_rules(info_data, rules):
195 elif 'ARM_ATSAM' in rules: 594 elif 'ARM_ATSAM' in rules:
196 info_data['platform'] = 'ARM_ATSAM' 595 info_data['platform'] = 'ARM_ATSAM'
197 596
597 if 'BOARD' in rules:
598 if 'board' in info_data:
599 _log_warning(info_data, 'Board is specified in both info.json and rules.mk, the rules.mk value wins.')
600
601 info_data['board'] = rules['BOARD']
602
198 return info_data 603 return info_data
199 604
200 605
@@ -204,9 +609,26 @@ def avr_processor_rules(info_data, rules):
204 info_data['processor_type'] = 'avr' 609 info_data['processor_type'] = 'avr'
205 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu' 610 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
206 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown' 611 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' 612 info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
209 613
614 if 'MCU' in rules:
615 if 'processor' in info_data:
616 _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
617
618 info_data['processor'] = rules['MCU']
619
620 elif 'processor' not in info_data:
621 info_data['processor'] = 'unknown'
622
623 if 'BOOTLOADER' in rules:
624 # FIXME(skullydazed/anyone): need to remove the massive amounts of duplication first
625 # if 'bootloader' in info_data:
626 # _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
627
628 info_data['bootloader'] = rules['BOOTLOADER']
629 else:
630 info_data['bootloader'] = 'atmel-dfu'
631
210 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk: 632 # 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' 633 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
212 634
@@ -225,38 +647,44 @@ def unknown_processor_rules(info_data, rules):
225 return info_data 647 return info_data
226 648
227 649
650def deep_update(origdict, newdict):
651 """Update a dictionary in place, recursing to do a deep copy.
652 """
653 for key, value in newdict.items():
654 if isinstance(value, Mapping):
655 origdict[key] = deep_update(origdict.get(key, {}), value)
656
657 else:
658 origdict[key] = value
659
660 return origdict
661
662
228def merge_info_jsons(keyboard, info_data): 663def merge_info_jsons(keyboard, info_data):
229 """Return a merged copy of all the info.json files for a keyboard. 664 """Return a merged copy of all the info.json files for a keyboard.
230 """ 665 """
231 for info_file in find_info_json(keyboard): 666 for info_file in find_info_json(keyboard):
232 # Load and validate the JSON data 667 # Load and validate the JSON data
233 try: 668 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 669
240 if not isinstance(new_info_data, dict): 670 if not isinstance(new_info_data, dict):
241 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) 671 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
242 continue 672 continue
243 673
244 # Copy whitelisted keys into `info_data` 674 try:
245 for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): 675 keyboard_validate(new_info_data)
246 if key in new_info_data: 676 except jsonschema.ValidationError as e:
247 info_data[key] = new_info_data[key] 677 json_path = '.'.join([str(p) for p in e.absolute_path])
678 cli.log.error('Not including data from file: %s', info_file)
679 cli.log.error('\t%s: %s', json_path, e.message)
680 continue
248 681
249 # Merge the layouts in 682 # Mark the layouts as coming from json
250 if 'layouts' in new_info_data: 683 for layout in new_info_data.get('layouts', {}).values():
251 for layout_name, json_layout in new_info_data['layouts'].items(): 684 layout['c_macro'] = False
252 # Only pull in layouts we have a macro for 685
253 if layout_name in info_data['layouts']: 686 # Update info_data with the new data
254 if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']): 687 deep_update(info_data, new_info_data)
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 688
261 return info_data 689 return info_data
262 690
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")},