aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/cli/c2json.py3
-rw-r--r--lib/python/qmk/cli/chibios/confmigrate.py8
-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.py49
-rwxr-xr-xlib/python/qmk/cli/generate/layouts.py94
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py59
-rwxr-xr-xlib/python/qmk/cli/info.py7
-rwxr-xr-xlib/python/qmk/cli/kle2json.py51
-rw-r--r--lib/python/qmk/constants.py11
-rw-r--r--lib/python/qmk/info.py358
-rwxr-xr-xlib/python/qmk/info_json_encoder.py96
13 files changed, 924 insertions, 107 deletions
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 eae294a0c..b9cfda961 100644
--- a/lib/python/qmk/cli/chibios/confmigrate.py
+++ b/lib/python/qmk/cli/chibios/confmigrate.py
@@ -13,7 +13,7 @@ def eprint(*args, **kwargs):
13 print(*args, file=sys.stderr, **kwargs) 13 print(*args, file=sys.stderr, **kwargs)
14 14
15 15
16fileHeader = """\ 16file_header = """\
17/* Copyright 2020 QMK 17/* Copyright 2020 QMK
18 * 18 *
19 * This program is free software: you can redistribute it and/or modify 19 * This program is free software: you can redistribute it and/or modify
@@ -77,7 +77,7 @@ def check_diffs(input_defs, reference_defs):
77 77
78 78
79def migrate_chconf_h(to_override, outfile): 79def migrate_chconf_h(to_override, outfile):
80 print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) 80 print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
81 81
82 for override in to_override: 82 for override in to_override:
83 print("#define %s %s" % (override[0], override[1]), file=outfile) 83 print("#define %s %s" % (override[0], override[1]), file=outfile)
@@ -87,7 +87,7 @@ def migrate_chconf_h(to_override, outfile):
87 87
88 88
89def migrate_halconf_h(to_override, outfile): 89def migrate_halconf_h(to_override, outfile):
90 print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) 90 print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
91 91
92 for override in to_override: 92 for override in to_override:
93 print("#define %s %s" % (override[0], override[1]), file=outfile) 93 print("#define %s %s" % (override[0], override[1]), file=outfile)
@@ -97,7 +97,7 @@ def migrate_halconf_h(to_override, outfile):
97 97
98 98
99def migrate_mcuconf_h(to_override, outfile): 99def migrate_mcuconf_h(to_override, outfile):
100 print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) 100 print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile)
101 101
102 print("#include_next <mcuconf.h>\n", file=outfile) 102 print("#include_next <mcuconf.h>\n", file=outfile)
103 103
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..09aa0041f
--- /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([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(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(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,))
177 rgblight_config.append('# define %s %s' % (config_key, config[json_key]))
178 rgblight_config.append('#endif // %s' % (config_key,))
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..7e6654e45
--- /dev/null
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -0,0 +1,49 @@
1"""Keyboard information script.
2
3Compile an info.json for a particular keyboard and pretty-print it.
4"""
5import json
6
7from milc import cli
8
9from qmk.info_json_encoder import InfoJSONEncoder
10from qmk.decorators import automagic_keyboard, automagic_keymap
11from qmk.info import info_json
12from qmk.path import is_keyboard
13
14
15@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')
16@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
17@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
18@automagic_keyboard
19@automagic_keymap
20def generate_info_json(cli):
21 """Generate an info.json file for a keyboard
22 """
23 # Determine our keyboard(s)
24 if not cli.config.generate_info_json.keyboard:
25 cli.log.error('Missing paramater: --keyboard')
26 cli.subcommands['info'].print_help()
27 return False
28
29 if not is_keyboard(cli.config.generate_info_json.keyboard):
30 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
31 return False
32
33 # Build the info.json file
34 kb_info_json = info_json(cli.config.generate_info_json.keyboard)
35 pared_down_json = {}
36
37 for key in ('manufacturer', 'maintainer', 'usb', 'keyboard_name', 'width', 'height', 'debounce', 'diode_direction', 'features', 'community_layouts', 'layout_aliases', 'matrix_pins', 'rgblight', 'url'):
38 if key in kb_info_json:
39 pared_down_json[key] = kb_info_json[key]
40
41 pared_down_json['layouts'] = {}
42 if 'layouts' in pared_down_json:
43 for layout_name, layout in kb_info_json['layouts'].items():
44 pared_down_json['layouts'][layout_name] = {}
45 pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))
46 pared_down_json['layouts'][layout_name]['layout'] = layout['layout']
47
48 # Display the results
49 print(json.dumps(pared_down_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..273870e15
--- /dev/null
+++ b/lib/python/qmk/cli/generate/layouts.py
@@ -0,0 +1,94 @@
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 layout_keys = []
58 layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)]
59
60 for i, key in enumerate(kb_info_json['layouts'][layout_name]['layout']):
61 row = key['matrix'][0]
62 col = key['matrix'][1]
63 identifier = 'k%s%s' % (ROW_LETTERS[row], COL_LETTERS[col])
64
65 try:
66 layout_matrix[row][col] = identifier
67 layout_keys.append(identifier)
68 except IndexError:
69 key_name = key.get('label', identifier)
70 cli.log.error('Matrix data out of bounds for layout %s at index %s (%s): %s, %s', layout_name, i, key_name, row, col)
71 return False
72
73 layouts_h_lines.append('')
74 layouts_h_lines.append('#define %s(%s) {\\' % (layout_name, ', '.join(layout_keys)))
75
76 rows = ', \\\n'.join(['\t {' + ', '.join(row) + '}' for row in layout_matrix])
77 rows += ' \\'
78 layouts_h_lines.append(rows)
79 layouts_h_lines.append('}')
80
81 # Show the results
82 layouts_h = '\n'.join(layouts_h_lines) + '\n'
83
84 if cli.args.output:
85 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
86 if cli.args.output.exists():
87 cli.args.output.replace(cli.args.output.name + '.bak')
88 cli.args.output.write_text(layouts_h)
89
90 if not cli.args.quiet:
91 cli.log.info('Wrote info_config.h to %s.', cli.args.output)
92
93 else:
94 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..4268ae047
--- /dev/null
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -0,0 +1,59 @@
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
9
10@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
11@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
12@cli.argument('-kb', '--keyboard', help='Keyboard to generate config.h for.')
13@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
14@automagic_keyboard
15@automagic_keymap
16def generate_rules_mk(cli):
17 """Generates a rules.mk file from info.json.
18 """
19 # Determine our keyboard(s)
20 if not cli.config.generate_rules_mk.keyboard:
21 cli.log.error('Missing paramater: --keyboard')
22 cli.subcommands['info'].print_help()
23 return False
24
25 if not is_keyboard(cli.config.generate_rules_mk.keyboard):
26 cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard)
27 return False
28
29 # Build the info.json file
30 kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
31 rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
32
33 # Find features that should be enabled
34 if 'features' in kb_info_json:
35 for feature, enabled in kb_info_json['features'].items():
36 feature = feature.upper()
37 enabled = 'yes' if enabled else 'no'
38 rules_mk_lines.append(f'{feature}_ENABLE := {enabled}')
39
40 # Add community layouts
41 if 'community_layouts' in kb_info_json:
42 rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')
43
44 # Show the results
45 rules_mk = '\n'.join(rules_mk_lines) + '\n'
46
47 if cli.args.output:
48 cli.args.output.parent.mkdir(parents=True, exist_ok=True)
49 if cli.args.output.exists():
50 cli.args.output.replace(cli.args.output.name + '.bak')
51 cli.args.output.write_text(rules_mk)
52
53 if cli.args.quiet:
54 print(cli.args.output)
55 else:
56 cli.log.info('Wrote info_config.h to %s.', cli.args.output)
57
58 else:
59 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 94ab68e5e..675832c50 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -17,3 +17,14 @@ VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
17DATE_FORMAT = '%Y-%m-%d' 17DATE_FORMAT = '%Y-%m-%d'
18DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z' 18DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
19TIME_FORMAT = '%H:%M:%S' 19TIME_FORMAT = '%H:%M:%S'
20
21# Used when generating matrix locations
22COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'
23ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop'
24
25# Mapping between info.json and config.h keys
26LED_INDICATORS = {
27 'caps_lock': 'LED_CAPS_LOCK_PIN',
28 'num_lock': 'LED_NUM_LOCK_PIN',
29 'scrol_lock': 'LED_SCROLL_LOCK_PIN'
30}
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index f476dc666..4611874e8 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -6,13 +6,45 @@ from pathlib import Path
6 6
7from milc import cli 7from milc import cli
8 8
9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
10from qmk.c_parse import find_layouts 10from qmk.c_parse import find_layouts
11from qmk.keyboard import config_h, rules_mk 11from qmk.keyboard import config_h, rules_mk
12from qmk.keymap import list_keymaps 12from qmk.keymap import list_keymaps
13from qmk.makefile import parse_rules_mk_file 13from qmk.makefile import parse_rules_mk_file
14from qmk.math import compute 14from qmk.math import compute
15 15
16rgblight_properties = {
17 'led_count': 'RGBLED_NUM',
18 'pin': 'RGB_DI_PIN',
19 'split_count': 'RGBLED_SPLIT',
20 'max_brightness': 'RGBLIGHT_LIMIT_VAL',
21 'hue_steps': 'RGBLIGHT_HUE_STEP',
22 'saturation_steps': 'RGBLIGHT_SAT_STEP',
23 'brightness_steps': 'RGBLIGHT_VAL_STEP'
24}
25
26rgblight_toggles = {
27 'sleep': 'RGBLIGHT_SLEEP',
28 'split': 'RGBLIGHT_SPLIT',
29}
30
31rgblight_animations = {
32 'all': 'RGBLIGHT_ANIMATIONS',
33 'alternating': 'RGBLIGHT_EFFECT_ALTERNATING',
34 'breathing': 'RGBLIGHT_EFFECT_BREATHING',
35 'christmas': 'RGBLIGHT_EFFECT_CHRISTMAS',
36 'knight': 'RGBLIGHT_EFFECT_KNIGHT',
37 'rainbow_mood': 'RGBLIGHT_EFFECT_RAINBOW_MOOD',
38 'rainbow_swirl': 'RGBLIGHT_EFFECT_RAINBOW_SWIRL',
39 'rgb_test': 'RGBLIGHT_EFFECT_RGB_TEST',
40 'snake': 'RGBLIGHT_EFFECT_SNAKE',
41 'static_gradient': 'RGBLIGHT_EFFECT_STATIC_GRADIENT',
42 'twinkle': 'RGBLIGHT_EFFECT_TWINKLE'
43}
44
45true_values = ['1', 'on', 'yes']
46false_values = ['0', 'off', 'no']
47
16 48
17def info_json(keyboard): 49def info_json(keyboard):
18 """Generate the info.json data for a specific keyboard. 50 """Generate the info.json data for a specific keyboard.
@@ -38,8 +70,9 @@ def info_json(keyboard):
38 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} 70 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
39 71
40 # Populate layout data 72 # Populate layout data
41 for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items(): 73 for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items():
42 if not layout_name.startswith('LAYOUT_kc'): 74 if not layout_name.startswith('LAYOUT_kc'):
75 layout_json['c_macro'] = True
43 info_data['layouts'][layout_name] = layout_json 76 info_data['layouts'][layout_name] = layout_json
44 77
45 # Merge in the data from info.json, config.h, and rules.mk 78 # Merge in the data from info.json, config.h, and rules.mk
@@ -47,34 +80,182 @@ def info_json(keyboard):
47 info_data = _extract_config_h(info_data) 80 info_data = _extract_config_h(info_data)
48 info_data = _extract_rules_mk(info_data) 81 info_data = _extract_rules_mk(info_data)
49 82
83 # Make sure we have at least one layout
84 if not info_data.get('layouts'):
85 _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
86
87 # Make sure we supply layout macros for the community layouts we claim to support
88 # FIXME(skullydazed): This should be populated into info.json and read from there instead
89 if 'LAYOUTS' in rules and info_data.get('layouts'):
90 # Match these up against the supplied layouts
91 supported_layouts = rules['LAYOUTS'].strip().split()
92 for layout_name in sorted(info_data['layouts']):
93 layout_name = layout_name[7:]
94
95 if layout_name in supported_layouts:
96 supported_layouts.remove(layout_name)
97
98 if supported_layouts:
99 for supported_layout in supported_layouts:
100 _log_error(info_data, 'Claims to support community layout %s but no LAYOUT_%s() macro found' % (supported_layout, supported_layout))
101
50 return info_data 102 return info_data
51 103
52 104
53def _extract_config_h(info_data): 105def _extract_debounce(info_data, config_c):
54 """Pull some keyboard information from existing rules.mk files 106 """Handle debounce.
107 """
108 if 'debounce' in info_data and 'DEBOUNCE' in config_c:
109 _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
110
111 if 'DEBOUNCE' in config_c:
112 info_data['debounce'] = config_c.get('DEBOUNCE')
113
114 return info_data
115
116
117def _extract_diode_direction(info_data, config_c):
118 """Handle the diode direction.
119 """
120 if 'diode_direction' in info_data and 'DIODE_DIRECTION' in config_c:
121 _log_warning(info_data, 'Diode direction is specified in both info.json and config.h, the config.h value wins.')
122
123 if 'DIODE_DIRECTION' in config_c:
124 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION')
125
126 return info_data
127
128
129def _extract_indicators(info_data, config_c):
130 """Find the LED indicator information.
131 """
132 for json_key, config_key in LED_INDICATORS.items():
133 if json_key in info_data.get('indicators', []) and config_key in config_c:
134 _log_warning(info_data, f'Indicator {json_key} is specified in both info.json and config.h, the config.h value wins.')
135
136 if config_key in config_c:
137 if 'indicators' not in info_data:
138 info_data['indicators'] = {}
139
140 info_data['indicators'][json_key] = config_c.get(config_key)
141
142 return info_data
143
144
145def _extract_community_layouts(info_data, rules):
146 """Find the community layouts in rules.mk.
147 """
148 community_layouts = rules['LAYOUTS'].split() if 'LAYOUTS' in rules else []
149
150 if 'community_layouts' in info_data:
151 for layout in community_layouts:
152 if layout not in info_data['community_layouts']:
153 community_layouts.append(layout)
154
155 else:
156 info_data['community_layouts'] = community_layouts
157
158 return info_data
159
160
161def _extract_features(info_data, rules):
162 """Find all the features enabled in rules.mk.
163 """
164 for key, value in rules.items():
165 if key.endswith('_ENABLE'):
166 key = '_'.join(key.split('_')[:-1]).lower()
167 value = True if value in true_values else False if value in false_values else value
168
169 if 'config_h_features' not in info_data:
170 info_data['config_h_features'] = {}
171
172 if 'features' not in info_data:
173 info_data['features'] = {}
174
175 if key in info_data['features']:
176 _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
177
178 info_data['features'][key] = value
179 info_data['config_h_features'][key] = value
180
181 return info_data
182
183
184def _extract_rgblight(info_data, config_c):
185 """Handle the rgblight configuration
186 """
187 rgblight = info_data.get('rgblight', {})
188 animations = rgblight.get('animations', {})
189
190 for json_key, config_key in rgblight_properties.items():
191 if config_key in config_c:
192 if json_key in rgblight:
193 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
194
195 rgblight[json_key] = config_c[config_key]
196
197 for json_key, config_key in rgblight_toggles.items():
198 if config_key in config_c:
199 if json_key in rgblight:
200 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key)
201
202 rgblight[json_key] = config_c[config_key]
203
204 for json_key, config_key in rgblight_animations.items():
205 if config_key in config_c:
206 if json_key in animations:
207 _log_warning(info_data, 'RGB Light: animations: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
208
209 animations[json_key] = config_c[config_key]
210
211 if animations:
212 rgblight['animations'] = animations
213
214 if rgblight:
215 info_data['rgblight'] = rgblight
216
217 return info_data
218
219
220def _extract_matrix_info(info_data, config_c):
221 """Populate the matrix information.
55 """ 222 """
56 config_c = config_h(info_data['keyboard_folder'])
57 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() 223 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
58 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() 224 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
59 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] 225 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
60 226
61 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION') 227 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
62 info_data['matrix_size'] = { 228 if 'matrix_size' in info_data:
63 'rows': compute(config_c.get('MATRIX_ROWS', '0')), 229 _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')), 230
65 } 231 info_data['matrix_size'] = {
66 info_data['matrix_pins'] = {} 232 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
233 'cols': compute(config_c.get('MATRIX_COLS', '0')),
234 }
235
236 if row_pins and col_pins:
237 if 'matrix_pins' in info_data:
238 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
239
240 info_data['matrix_pins'] = {}
67 241
68 if row_pins: 242 if row_pins:
69 info_data['matrix_pins']['rows'] = row_pins.split(',') 243 info_data['matrix_pins']['rows'] = row_pins.split(',')
70 if col_pins: 244
71 info_data['matrix_pins']['cols'] = col_pins.split(',') 245 if col_pins:
246 info_data['matrix_pins']['cols'] = col_pins.split(',')
72 247
73 if direct_pins: 248 if direct_pins:
249 if 'matrix_pins' in info_data:
250 _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
251
252 info_data['matrix_pins'] = {}
74 direct_pin_array = [] 253 direct_pin_array = []
254
75 for row in direct_pins.split('},{'): 255 for row in direct_pins.split('},{'):
76 if row.startswith('{'): 256 if row.startswith('{'):
77 row = row[1:] 257 row = row[1:]
258
78 if row.endswith('}'): 259 if row.endswith('}'):
79 row = row[:-1] 260 row = row[:-1]
80 261
@@ -86,15 +267,43 @@ def _extract_config_h(info_data):
86 267
87 direct_pin_array[-1].append(pin) 268 direct_pin_array[-1].append(pin)
88 269
89 info_data['matrix_pins']['direct'] = direct_pin_array 270 info_data['matrix_pins']['direct'] = direct_pin_array
90 271
91 info_data['usb'] = { 272 return info_data
92 'vid': config_c.get('VENDOR_ID'), 273
93 'pid': config_c.get('PRODUCT_ID'), 274
94 'device_ver': config_c.get('DEVICE_VER'), 275def _extract_usb_info(info_data, config_c):
95 'manufacturer': config_c.get('MANUFACTURER'), 276 """Populate the USB information.
96 'product': config_c.get('PRODUCT'), 277 """
97 } 278 usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'}
279
280 if 'usb' not in info_data:
281 info_data['usb'] = {}
282
283 for info_name, config_name in usb_properties.items():
284 if config_name in config_c:
285 if info_name in info_data['usb']:
286 _log_warning(info_data, '%s in config.h is overwriting usb.%s in info.json' % (config_name, info_name))
287
288 info_data['usb'][info_name] = config_c[config_name]
289
290 elif info_name not in info_data['usb']:
291 _log_error(info_data, '%s not specified in config.h, and %s not specified in info.json. One is required.' % (config_name, info_name))
292
293 return info_data
294
295
296def _extract_config_h(info_data):
297 """Pull some keyboard information from existing config.h files
298 """
299 config_c = config_h(info_data['keyboard_folder'])
300
301 _extract_debounce(info_data, config_c)
302 _extract_diode_direction(info_data, config_c)
303 _extract_indicators(info_data, config_c)
304 _extract_matrix_info(info_data, config_c)
305 _extract_usb_info(info_data, config_c)
306 _extract_rgblight(info_data, config_c)
98 307
99 return info_data 308 return info_data
100 309
@@ -106,16 +315,52 @@ def _extract_rules_mk(info_data):
106 mcu = rules.get('MCU') 315 mcu = rules.get('MCU')
107 316
108 if mcu in CHIBIOS_PROCESSORS: 317 if mcu in CHIBIOS_PROCESSORS:
109 return arm_processor_rules(info_data, rules) 318 arm_processor_rules(info_data, rules)
110 319
111 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: 320 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
112 return avr_processor_rules(info_data, rules) 321 avr_processor_rules(info_data, rules)
322
323 else:
324 cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu))
325 unknown_processor_rules(info_data, rules)
326
327 _extract_community_layouts(info_data, rules)
328 _extract_features(info_data, rules)
329
330 return info_data
113 331
114 msg = "Unknown MCU: " + str(mcu)
115 332
116 _log_warning(info_data, msg) 333def _merge_layouts(info_data, new_info_data):
334 """Merge new_info_data into info_data in an intelligent way.
335 """
336 for layout_name, layout_json in new_info_data['layouts'].items():
337 if layout_name in info_data['layouts']:
338 # Pull in layouts we have a macro for
339 if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
340 msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
341 _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
342 else:
343 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
344 key.update(layout_json['layout'][i])
345 else:
346 # Pull in layouts that have matrix data
347 missing_matrix = False
348 for key in layout_json.get('layout', {}):
349 if 'matrix' not in key:
350 missing_matrix = True
351
352 if not missing_matrix:
353 if layout_name in info_data['layouts']:
354 # Update an existing layout with new data
355 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
356 key.update(layout_json['layout'][i])
117 357
118 return unknown_processor_rules(info_data, rules) 358 else:
359 # Copy in the new layout wholesale
360 layout_json['c_macro'] = False
361 info_data['layouts'][layout_name] = layout_json
362
363 return info_data
119 364
120 365
121def _search_keyboard_h(path): 366def _search_keyboard_h(path):
@@ -131,34 +376,21 @@ def _search_keyboard_h(path):
131 return layouts 376 return layouts
132 377
133 378
134def _find_all_layouts(info_data, keyboard, rules): 379def _find_all_layouts(info_data, keyboard):
135 """Looks for layout macros associated with this keyboard. 380 """Looks for layout macros associated with this keyboard.
136 """ 381 """
137 layouts = _search_keyboard_h(Path(keyboard)) 382 layouts = _search_keyboard_h(Path(keyboard))
138 383
139 if not layouts: 384 if not layouts:
140 # If we didn't find any layouts above we widen our search. This is error 385 # 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. 386 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.') 387
143 for file in glob('keyboards/%s/*.h' % keyboard): 388 for file in glob('keyboards/%s/*.h' % keyboard):
144 if file.endswith('.h'): 389 if file.endswith('.h'):
145 these_layouts = find_layouts(file) 390 these_layouts = find_layouts(file)
146 if these_layouts: 391 if these_layouts:
147 layouts.update(these_layouts) 392 layouts.update(these_layouts)
148 393
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 394 return layouts
163 395
164 396
@@ -231,32 +463,40 @@ def merge_info_jsons(keyboard, info_data):
231 for info_file in find_info_json(keyboard): 463 for info_file in find_info_json(keyboard):
232 # Load and validate the JSON data 464 # Load and validate the JSON data
233 try: 465 try:
234 with info_file.open('r') as info_fd: 466 new_info_data = json.load(info_file.open('r'))
235 new_info_data = json.load(info_fd)
236 except Exception as e: 467 except Exception as e:
237 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e)) 468 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
238 continue 469 new_info_data = {}
239 470
240 if not isinstance(new_info_data, dict): 471 if not isinstance(new_info_data, dict):
241 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) 472 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
242 continue 473 continue
243 474
244 # Copy whitelisted keys into `info_data` 475 # Copy whitelisted keys into `info_data`
245 for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): 476 for key in ('debounce', 'diode_direction', 'indicators', 'keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'):
246 if key in new_info_data: 477 if key in new_info_data:
247 info_data[key] = new_info_data[key] 478 info_data[key] = new_info_data[key]
248 479
249 # Merge the layouts in 480 # Deep merge certain keys
481 # FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something.
482 for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'):
483 if key in new_info_data:
484 if key not in info_data:
485 info_data[key] = {}
486
487 info_data[key].update(new_info_data[key])
488
489 # Merge the layouts
490 if 'community_layouts' in new_info_data:
491 if 'community_layouts' in info_data:
492 for layout in new_info_data['community_layouts']:
493 if layout not in info_data['community_layouts']:
494 info_data['community_layouts'].append(layout)
495 else:
496 info_data['community_layouts'] = new_info_data['community_layouts']
497
250 if 'layouts' in new_info_data: 498 if 'layouts' in new_info_data:
251 for layout_name, json_layout in new_info_data['layouts'].items(): 499 _merge_layouts(info_data, new_info_data)
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 500
261 return info_data 501 return info_data
262 502
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)