Merge remote-tracking branch 'upstream/develop' into xap

This commit is contained in:
Nick Brassel 2021-09-15 11:40:29 +10:00
commit 3c66b9b0ec
7256 changed files with 77233 additions and 46094 deletions

View file

@ -74,7 +74,6 @@ def find_layouts(file):
key['matrix'] = matrix_locations[key['label']]
parsed_layouts[macro_name] = {
'key_count': len(parsed_layout),
'layout': parsed_layout,
'filename': str(file),
}

View file

@ -35,7 +35,6 @@ subcommands = [
'qmk.cli.chibios.confmigrate',
'qmk.cli.clean',
'qmk.cli.compile',
'qmk.cli.console',
'qmk.cli.docs',
'qmk.cli.doctor',
'qmk.cli.fileformat',
@ -72,6 +71,26 @@ subcommands = [
]
def _install_deps(requirements):
"""Perform the installation of missing requirements.
If we detect that we are running in a virtualenv we can't write into we'll use sudo to perform the pip install.
"""
command = [sys.executable, '-m', 'pip', 'install']
if sys.prefix != sys.base_prefix:
# We are in a virtualenv, check to see if we need to use sudo to write to it
if not os.access(sys.prefix, os.W_OK):
print('Notice: Using sudo to install modules to location owned by root:', sys.prefix)
command.insert(0, 'sudo')
elif not os.access(sys.prefix, os.W_OK):
# We can't write to sys.prefix, attempt to install locally
command.append('--local')
return _run_cmd(*command, '-r', requirements)
def _run_cmd(*command):
"""Run a command in a subshell.
"""
@ -164,8 +183,14 @@ if int(milc_version[0]) < 2 and int(milc_version[1]) < 4:
print(f'Your MILC library is too old! Please upgrade: python3 -m pip install -U -r {str(requirements)}')
exit(127)
# Make sure we can run binaries in the same directory as our Python interpreter
python_dir = os.path.dirname(sys.executable)
if python_dir not in os.environ['PATH'].split(':'):
os.environ['PATH'] = ":".join((python_dir, os.environ['PATH']))
# Check to make sure we have all our dependencies
msg_install = 'Please run `python3 -m pip install -r %s` to install required python dependencies.'
msg_install = f'Please run `{sys.executable} -m pip install -r %s` to install required python dependencies.'
args = sys.argv[1:]
while args and args[0][0] == '-':
del args[0]
@ -175,7 +200,7 @@ safe_command = args and args[0] in safe_commands
if not safe_command:
if _broken_module_imports('requirements.txt'):
if yesno('Would you like to install the required Python modules?'):
_run_cmd(sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt')
_install_deps('requirements.txt')
else:
print()
print(msg_install % (str(Path('requirements.txt').resolve()),))
@ -184,7 +209,7 @@ if not safe_command:
if cli.config.user.developer and _broken_module_imports('requirements-dev.txt'):
if yesno('Would you like to install the required developer Python modules?'):
_run_cmd(sys.executable, '-m', 'pip', 'install', '-r', 'requirements-dev.txt')
_install_deps('requirements-dev.txt')
elif yesno('Would you like to disable developer mode?'):
_run_cmd(sys.argv[0], 'config', 'user.developer=None')
else:

View file

@ -18,7 +18,7 @@ from qmk.keymap import keymap_completer
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Ignored when a configurator export is supplied.')
@cli.argument('-km', '--keymap', completer=keymap_completer, help='The keymap to build a firmware for. Ignored when a configurator export is supplied.')
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
@cli.subcommand('Compile a QMK Firmware.')

View file

@ -1,302 +0,0 @@
"""Acquire debugging information from usb hid devices
cli implementation of https://www.pjrc.com/teensy/hid_listen.html
"""
from pathlib import Path
from threading import Thread
from time import sleep, strftime
import hid
import usb.core
from milc import cli
LOG_COLOR = {
'next': 0,
'colors': [
'{fg_blue}',
'{fg_cyan}',
'{fg_green}',
'{fg_magenta}',
'{fg_red}',
'{fg_yellow}',
],
}
KNOWN_BOOTLOADERS = {
# VID , PID
('03EB', '2FEF'): 'atmel-dfu: ATmega16U2',
('03EB', '2FF0'): 'atmel-dfu: ATmega32U2',
('03EB', '2FF3'): 'atmel-dfu: ATmega16U4',
('03EB', '2FF4'): 'atmel-dfu: ATmega32U4',
('03EB', '2FF9'): 'atmel-dfu: AT90USB64',
('03EB', '2FFA'): 'atmel-dfu: AT90USB162',
('03EB', '2FFB'): 'atmel-dfu: AT90USB128',
('03EB', '6124'): 'Microchip SAM-BA',
('0483', 'DF11'): 'stm32-dfu: STM32 BOOTLOADER',
('16C0', '05DC'): 'USBasp: USBaspLoader',
('16C0', '05DF'): 'bootloadHID: HIDBoot',
('16C0', '0478'): 'halfkay: Teensy Halfkay',
('1B4F', '9203'): 'caterina: Pro Micro 3.3V',
('1B4F', '9205'): 'caterina: Pro Micro 5V',
('1B4F', '9207'): 'caterina: LilyPadUSB',
('1C11', 'B007'): 'kiibohd: Kiibohd DFU Bootloader',
('1EAF', '0003'): 'stm32duino: Maple 003',
('1FFB', '0101'): 'caterina: Polou A-Star 32U4 Bootloader',
('2341', '0036'): 'caterina: Arduino Leonardo',
('2341', '0037'): 'caterina: Arduino Micro',
('239A', '000C'): 'caterina: Adafruit Feather 32U4',
('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v',
('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
('2A03', '0036'): 'caterina: Arduino Leonardo',
('2A03', '0037'): 'caterina: Arduino Micro',
('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode'
}
class MonitorDevice(object):
def __init__(self, hid_device, numeric):
self.hid_device = hid_device
self.numeric = numeric
self.device = hid.Device(path=hid_device['path'])
self.current_line = ''
cli.log.info('Console Connected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', hid_device)
def read(self, size, encoding='ascii', timeout=1):
"""Read size bytes from the device.
"""
return self.device.read(size, timeout).decode(encoding)
def read_line(self):
"""Read from the device's console until we get a \n.
"""
while '\n' not in self.current_line:
self.current_line += self.read(32).replace('\x00', '')
lines = self.current_line.split('\n', 1)
self.current_line = lines[1]
return lines[0]
def run_forever(self):
while True:
try:
message = {**self.hid_device, 'text': self.read_line()}
identifier = (int2hex(message['vendor_id']), int2hex(message['product_id'])) if self.numeric else (message['manufacturer_string'], message['product_string'])
message['identifier'] = ':'.join(identifier)
message['ts'] = '{style_dim}{fg_green}%s{style_reset_all} ' % (strftime(cli.config.general.datetime_fmt),) if cli.args.timestamp else ''
cli.echo('%(ts)s%(color)s%(identifier)s:%(index)d{style_reset_all}: %(text)s' % message)
except hid.HIDException:
break
class FindDevices(object):
def __init__(self, vid, pid, index, numeric):
self.vid = vid
self.pid = pid
self.index = index
self.numeric = numeric
def run_forever(self):
"""Process messages from our queue in a loop.
"""
live_devices = {}
live_bootloaders = {}
while True:
try:
for device in list(live_devices):
if not live_devices[device]['thread'].is_alive():
cli.log.info('Console Disconnected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', live_devices[device])
del live_devices[device]
for device in self.find_devices():
if device['path'] not in live_devices:
device['color'] = LOG_COLOR['colors'][LOG_COLOR['next']]
LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
live_devices[device['path']] = device
try:
monitor = MonitorDevice(device, self.numeric)
device['thread'] = Thread(target=monitor.run_forever, daemon=True)
device['thread'].start()
except Exception as e:
device['e'] = e
device['e_name'] = e.__class__.__name__
cli.log.error("Could not connect to %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s:%(vendor_id)04X:%(product_id)04X:%(index)d): %(e_name)s: %(e)s", device)
if cli.config.general.verbose:
cli.log.exception(e)
del live_devices[device['path']]
if cli.args.bootloaders:
for device in self.find_bootloaders():
if device.address in live_bootloaders:
live_bootloaders[device.address]._qmk_found = True
else:
name = KNOWN_BOOTLOADERS[(int2hex(device.idVendor), int2hex(device.idProduct))]
cli.log.info('Bootloader Connected: {style_bright}{fg_magenta}%s', name)
device._qmk_found = True
live_bootloaders[device.address] = device
for device in list(live_bootloaders):
if live_bootloaders[device]._qmk_found:
live_bootloaders[device]._qmk_found = False
else:
name = KNOWN_BOOTLOADERS[(int2hex(live_bootloaders[device].idVendor), int2hex(live_bootloaders[device].idProduct))]
cli.log.info('Bootloader Disconnected: {style_bright}{fg_magenta}%s', name)
del live_bootloaders[device]
sleep(.1)
except KeyboardInterrupt:
break
def is_bootloader(self, hid_device):
"""Returns true if the device in question matches a known bootloader vid/pid.
"""
return (int2hex(hid_device.idVendor), int2hex(hid_device.idProduct)) in KNOWN_BOOTLOADERS
def is_console_hid(self, hid_device):
"""Returns true when the usage page indicates it's a teensy-style console.
"""
return hid_device['usage_page'] == 0xFF31 and hid_device['usage'] == 0x0074
def is_filtered_device(self, hid_device):
"""Returns True if the device should be included in the list of available consoles.
"""
return int2hex(hid_device['vendor_id']) == self.vid and int2hex(hid_device['product_id']) == self.pid
def find_devices_by_report(self, hid_devices):
"""Returns a list of available teensy-style consoles by doing a brute-force search.
Some versions of linux don't report usage and usage_page. In that case we fallback to reading the report (possibly inaccurately) ourselves.
"""
devices = []
for device in hid_devices:
path = device['path'].decode('utf-8')
if path.startswith('/dev/hidraw'):
number = path[11:]
report = Path(f'/sys/class/hidraw/hidraw{number}/device/report_descriptor')
if report.exists():
rp = report.read_bytes()
if rp[1] == 0x31 and rp[3] == 0x09:
devices.append(device)
return devices
def find_bootloaders(self):
"""Returns a list of available bootloader devices.
"""
return list(filter(self.is_bootloader, usb.core.find(find_all=True)))
def find_devices(self):
"""Returns a list of available teensy-style consoles.
"""
hid_devices = hid.enumerate()
devices = list(filter(self.is_console_hid, hid_devices))
if not devices:
devices = self.find_devices_by_report(hid_devices)
if self.vid and self.pid:
devices = list(filter(self.is_filtered_device, devices))
# Add index numbers
device_index = {}
for device in devices:
id = ':'.join((int2hex(device['vendor_id']), int2hex(device['product_id'])))
if id not in device_index:
device_index[id] = 0
device_index[id] += 1
device['index'] = device_index[id]
return devices
def int2hex(number):
"""Returns a string representation of the number as hex.
"""
return "%04X" % number
def list_devices(device_finder):
"""Show the user a nicely formatted list of devices.
"""
devices = device_finder.find_devices()
if devices:
cli.log.info('Available devices:')
for dev in devices:
color = LOG_COLOR['colors'][LOG_COLOR['next']]
LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
cli.log.info("\t%s%s:%s:%d{style_reset_all}\t%s %s", color, int2hex(dev['vendor_id']), int2hex(dev['product_id']), dev['index'], dev['manufacturer_string'], dev['product_string'])
if cli.args.bootloaders:
bootloaders = device_finder.find_bootloaders()
if bootloaders:
cli.log.info('Available Bootloaders:')
for dev in bootloaders:
cli.log.info("\t%s:%s\t%s", int2hex(dev.idVendor), int2hex(dev.idProduct), KNOWN_BOOTLOADERS[(int2hex(dev.idVendor), int2hex(dev.idProduct))])
@cli.argument('--bootloaders', arg_only=True, default=True, action='store_boolean', help='displaying bootloaders.')
@cli.argument('-d', '--device', help='Device to select - uses format <pid>:<vid>[:<index>].')
@cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.')
@cli.argument('-n', '--numeric', arg_only=True, action='store_true', help='Show VID/PID instead of names.')
@cli.argument('-t', '--timestamp', arg_only=True, action='store_true', help='Print the timestamp for received messages as well.')
@cli.argument('-w', '--wait', type=int, default=1, help="How many seconds to wait between checks (Default: 1)")
@cli.subcommand('Acquire debugging information from usb hid devices.', hidden=False if cli.config.user.developer else True)
def console(cli):
"""Acquire debugging information from usb hid devices
"""
vid = None
pid = None
index = 1
if cli.config.console.device:
device = cli.config.console.device.split(':')
if len(device) == 2:
vid, pid = device
elif len(device) == 3:
vid, pid, index = device
if not index.isdigit():
cli.log.error('Device index must be a number! Got "%s" instead.', index)
exit(1)
index = int(index)
if index < 1:
cli.log.error('Device index must be greater than 0! Got %s', index)
exit(1)
else:
cli.log.error('Invalid format for device, expected "<pid>:<vid>[:<index>]" but got "%s".', cli.config.console.device)
cli.print_help()
exit(1)
vid = vid.upper()
pid = pid.upper()
device_finder = FindDevices(vid, pid, index, cli.args.numeric)
if cli.args.list:
return list_devices(device_finder)
print('Looking for devices...', flush=True)
device_finder.run_forever()

View file

@ -26,7 +26,6 @@ ESSENTIAL_BINARIES = {
'arm-none-eabi-gcc': {
'version_arg': '-dumpversion'
},
'bin/qmk': {},
}

View file

@ -82,6 +82,10 @@ def check_udev_rules():
# dog hunter AG
_udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
_udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro
},
'hid-bootloader': {
_udev_rule("03eb", "2067"), # QMK HID
_udev_rule("16c0", "0478") # PJRC halfkay
}
}

View file

@ -38,7 +38,7 @@ def print_bootloader_help():
@cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.')
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
@cli.argument('-e', '--env', arg_only=True, action='append', default=[], help="Set a variable to be passed to make. May be passed multiple times.")
@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
@cli.subcommand('QMK Flash.')

View file

@ -12,7 +12,7 @@ from qmk.c_parse import c_source_files
c_file_suffixes = ('c', 'h', 'cpp')
core_dirs = ('drivers', 'quantum', 'tests', 'tmk_core', 'platforms')
ignored = ('tmk_core/protocol/usb_hid', 'quantum/template', 'platforms/chibios')
ignored = ('tmk_core/protocol/usb_hid', 'platforms/chibios/boards')
def find_clang_format():

View file

@ -11,15 +11,15 @@ def format_python(cli):
"""Format python code according to QMK's style.
"""
edit = '--diff' if cli.args.dry_run else '--in-place'
yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'bin/qmk', 'lib/python']
yapf_cmd = ['yapf', '-vv', '--recursive', edit, 'lib/python']
try:
cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL)
cli.log.info('Python code in `bin/qmk` and `lib/python` is correctly formatted.')
cli.log.info('Python code in `lib/python` is correctly formatted.')
return True
except CalledProcessError:
if cli.args.dry_run:
cli.log.error('Python code in `bin/qmk` and `lib/python` incorrectly formatted!')
cli.log.error('Python code in `lib/python` is incorrectly formatted!')
else:
cli.log.error('Error formatting python code!')

View file

@ -5,14 +5,14 @@ from pathlib import Path
from dotty_dict import dotty
from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.json_schema import json_load
from qmk.json_schema import json_load, validate
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import is_keyboard, normpath
from qmk.keymap import locate_keymap
from qmk.path import normpath
def direct_pins(direct_pins):
def direct_pins(direct_pins, postfix):
"""Return the config.h lines that set the direct pins.
"""
rows = []
@ -24,81 +24,60 @@ def direct_pins(direct_pins):
col_count = len(direct_pins[0])
row_count = len(direct_pins)
return """
#ifndef MATRIX_COLS
# define MATRIX_COLS %s
#endif // MATRIX_COLS
return f"""
#ifndef MATRIX_COLS{postfix}
# define MATRIX_COLS{postfix} {col_count}
#endif // MATRIX_COLS{postfix}
#ifndef MATRIX_ROWS
# define MATRIX_ROWS %s
#endif // MATRIX_ROWS
#ifndef MATRIX_ROWS{postfix}
# define MATRIX_ROWS{postfix} {row_count}
#endif // MATRIX_ROWS{postfix}
#ifndef DIRECT_PINS
# define DIRECT_PINS {%s}
#endif // DIRECT_PINS
""" % (col_count, row_count, ','.join(rows))
#ifndef DIRECT_PINS{postfix}
# define DIRECT_PINS{postfix} {{ {", ".join(rows)} }}
#endif // DIRECT_PINS{postfix}
"""
def pin_array(define, pins):
def pin_array(define, pins, postfix):
"""Return the config.h lines that set a pin array.
"""
pin_num = len(pins)
pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
return f"""
#ifndef {define}S
# define {define}S {pin_num}
#endif // {define}S
#ifndef {define}S{postfix}
# define {define}S{postfix} {pin_num}
#endif // {define}S{postfix}
#ifndef {define}_PINS
# define {define}_PINS {{ {pin_array} }}
#endif // {define}_PINS
#ifndef {define}_PINS{postfix}
# define {define}_PINS{postfix} {{ {pin_array} }}
#endif // {define}_PINS{postfix}
"""
def matrix_pins(matrix_pins):
def matrix_pins(matrix_pins, postfix=''):
"""Add the matrix config to the config.h.
"""
pins = []
if 'direct' in matrix_pins:
pins.append(direct_pins(matrix_pins['direct']))
pins.append(direct_pins(matrix_pins['direct'], postfix))
if 'cols' in matrix_pins:
pins.append(pin_array('MATRIX_COL', matrix_pins['cols']))
pins.append(pin_array('MATRIX_COL', matrix_pins['cols'], postfix))
if 'rows' in matrix_pins:
pins.append(pin_array('MATRIX_ROW', matrix_pins['rows']))
pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'], postfix))
return '\n'.join(pins)
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
@automagic_keyboard
@automagic_keymap
def generate_config_h(cli):
"""Generates the info_config.h file.
def generate_config_items(kb_info_json, config_h_lines):
"""Iterate through the info_config map to generate basic config values.
"""
# Determine our keyboard(s)
if not cli.config.generate_config_h.keyboard:
cli.log.error('Missing parameter: --keyboard')
cli.subcommands['info'].print_help()
return False
if not is_keyboard(cli.config.generate_config_h.keyboard):
cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
return False
# Build the info_config.h file.
kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
info_config_map = json_load(Path('data/mappings/info_config.json'))
config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
# Iterate through the info_config map to generate basic things
for config_key, info_dict in info_config_map.items():
info_key = info_dict['info_key']
key_type = info_dict.get('value_type', 'str')
@ -135,9 +114,75 @@ def generate_config_h(cli):
config_h_lines.append(f'# define {config_key} {config_value}')
config_h_lines.append(f'#endif // {config_key}')
def generate_split_config(kb_info_json, config_h_lines):
"""Generate the config.h lines for split boards."""
if 'primary' in kb_info_json['split']:
if kb_info_json['split']['primary'] in ('left', 'right'):
config_h_lines.append('')
config_h_lines.append('#ifndef MASTER_LEFT')
config_h_lines.append('# ifndef MASTER_RIGHT')
if kb_info_json['split']['primary'] == 'left':
config_h_lines.append('# define MASTER_LEFT')
elif kb_info_json['split']['primary'] == 'right':
config_h_lines.append('# define MASTER_RIGHT')
config_h_lines.append('# endif // MASTER_RIGHT')
config_h_lines.append('#endif // MASTER_LEFT')
elif kb_info_json['split']['primary'] == 'pin':
config_h_lines.append('')
config_h_lines.append('#ifndef SPLIT_HAND_PIN')
config_h_lines.append('# define SPLIT_HAND_PIN')
config_h_lines.append('#endif // SPLIT_HAND_PIN')
elif kb_info_json['split']['primary'] == 'matrix_grid':
config_h_lines.append('')
config_h_lines.append('#ifndef SPLIT_HAND_MATRIX_GRID')
config_h_lines.append('# define SPLIT_HAND_MATRIX_GRID {%s}' % (','.join(kb_info_json["split"]["matrix_grid"],)))
config_h_lines.append('#endif // SPLIT_HAND_MATRIX_GRID')
elif kb_info_json['split']['primary'] == 'eeprom':
config_h_lines.append('')
config_h_lines.append('#ifndef EE_HANDS')
config_h_lines.append('# define EE_HANDS')
config_h_lines.append('#endif // EE_HANDS')
if 'protocol' in kb_info_json['split'].get('transport', {}):
if kb_info_json['split']['transport']['protocol'] == 'i2c':
config_h_lines.append('')
config_h_lines.append('#ifndef USE_I2C')
config_h_lines.append('# define USE_I2C')
config_h_lines.append('#endif // USE_I2C')
if 'right' in kb_info_json['split'].get('matrix_pins', {}):
config_h_lines.append(matrix_pins(kb_info_json['split']['matrix_pins']['right'], '_RIGHT'))
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate config.h for.')
@cli.argument('-km', '--keymap', arg_only=True, help='Keymap to generate config.h for.')
@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
def generate_config_h(cli):
"""Generates the info_config.h file.
"""
# Determine our keyboard/keymap
if cli.args.keymap:
km = locate_keymap(cli.args.keyboard, cli.args.keymap)
km_json = json_load(km)
validate(km_json, 'qmk.keymap.v1')
kb_info_json = dotty(km_json.get('config', {}))
else:
kb_info_json = dotty(info_json(cli.args.keyboard))
# Build the info_config.h file.
config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
generate_config_items(kb_info_json, config_h_lines)
if 'matrix_pins' in kb_info_json:
config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
if 'split' in kb_info_json:
generate_split_config(kb_info_json, config_h_lines)
# Show the results
config_h = '\n'.join(config_h_lines)

View file

@ -4,15 +4,17 @@ Compile an info.json for a particular keyboard and pretty-print it.
"""
import json
from jsonschema import Draft7Validator, validators
from argcomplete.completers import FilesCompleter
from jsonschema import Draft7Validator, RefResolver, validators
from milc import cli
from pathlib import Path
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.json_encoders import InfoJSONEncoder
from qmk.json_schema import load_jsonschema
from qmk.json_schema import compile_schema_store
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import is_keyboard
from qmk.path import is_keyboard, normpath
def pruning_validator(validator_class):
@ -34,15 +36,19 @@ def pruning_validator(validator_class):
def strip_info_json(kb_info_json):
"""Remove the API-only properties from the info.json.
"""
schema_store = compile_schema_store()
pruning_draft_7_validator = pruning_validator(Draft7Validator)
schema = load_jsonschema('keyboard')
validator = pruning_draft_7_validator(schema).validate
schema = schema_store['qmk.keyboard.v1']
resolver = RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
validator = pruning_draft_7_validator(schema, resolver=resolver).validate
return validator(kb_info_json)
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.')
@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
@cli.argument('-o', '--output', arg_only=True, completer=FilesCompleter, help='Write the output the specified file, overwriting if necessary.')
@cli.argument('-ow', '--overwrite', arg_only=True, action='store_true', help='Overwrite the existing info.json. (Overrides the location of --output)')
@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
@automagic_keyboard
@automagic_keymap
@ -59,9 +65,29 @@ def generate_info_json(cli):
cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
return False
if cli.args.overwrite:
output_path = (Path('keyboards') / cli.config.generate_info_json.keyboard / 'info.json').resolve()
if cli.args.output:
cli.log.warning('Overwriting user supplied --output with %s', output_path)
cli.args.output = output_path
# Build the info.json file
kb_info_json = info_json(cli.config.generate_info_json.keyboard)
strip_info_json(kb_info_json)
info_json_text = json.dumps(kb_info_json, indent=4, cls=InfoJSONEncoder)
# Display the results
print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder))
if cli.args.output:
# Write to a file
output_path = normpath(cli.args.output)
if output_path.exists():
cli.log.warning('Overwriting output file %s', output_path)
output_path.write_text(info_json_text + '\n')
cli.log.info('Wrote info.json to %s.', output_path)
else:
# Display the results
print(info_json_text)

View file

@ -2,7 +2,6 @@
"""
from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import normpath
@ -29,14 +28,12 @@ def would_populate_layout_h(keyboard):
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.h for.')
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.h for.')
@cli.subcommand('Used by the make system to generate keyboard.h from info.json', hidden=True)
@automagic_keyboard
@automagic_keymap
def generate_keyboard_h(cli):
"""Generates the keyboard.h file.
"""
has_layout_h = would_populate_layout_h(cli.config.generate_keyboard_h.keyboard)
has_layout_h = would_populate_layout_h(cli.args.keyboard)
# Build the layouts.h file.
keyboard_h_lines = ['/* This file was generated by `qmk generate-keyboard-h`. Do not edit or copy.' ' */', '', '#pragma once', '#include "quantum.h"']

View file

@ -5,11 +5,11 @@ from pathlib import Path
from dotty_dict import dotty
from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.json_schema import json_load
from qmk.json_schema import json_load, validate
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import is_keyboard, normpath
from qmk.keymap import locate_keymap
from qmk.path import normpath
def process_mapping_rule(kb_info_json, rules_key, info_dict):
@ -39,23 +39,21 @@ def process_mapping_rule(kb_info_json, rules_key, info_dict):
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-e', '--escape', arg_only=True, action='store_true', help="Escape spaces in quiet mode")
@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
@automagic_keyboard
@automagic_keymap
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate rules.mk for.')
@cli.argument('-km', '--keymap', arg_only=True, help='Keymap to generate rules.mk for.')
@cli.subcommand('Used by the make system to generate rules.mk from info.json', hidden=True)
def generate_rules_mk(cli):
"""Generates a rules.mk file from info.json.
"""
if not cli.config.generate_rules_mk.keyboard:
cli.log.error('Missing parameter: --keyboard')
cli.subcommands['info'].print_help()
return False
# Determine our keyboard/keymap
if cli.args.keymap:
km = locate_keymap(cli.args.keyboard, cli.args.keymap)
km_json = json_load(km)
validate(km_json, 'qmk.keymap.v1')
kb_info_json = dotty(km_json.get('config', {}))
else:
kb_info_json = dotty(info_json(cli.args.keyboard))
if not is_keyboard(cli.config.generate_rules_mk.keyboard):
cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard)
return False
kb_info_json = dotty(info_json(cli.config.generate_rules_mk.keyboard))
info_rules_map = json_load(Path('data/mappings/info_rules.json'))
rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
@ -76,6 +74,17 @@ def generate_rules_mk(cli):
enabled = 'yes' if enabled else 'no'
rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
# Set SPLIT_TRANSPORT, if needed
if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom':
rules_mk_lines.append('SPLIT_TRANSPORT ?= custom')
# Set CUSTOM_MATRIX, if needed
if kb_info_json.get('matrix_pins', {}).get('custom'):
if kb_info_json.get('matrix_pins', {}).get('custom_lite'):
rules_mk_lines.append('CUSTOM_MATRIX ?= lite')
else:
rules_mk_lines.append('CUSTOM_MATRIX ?= yes')
# Show the results
rules_mk = '\n'.join(rules_mk_lines) + '\n'

View file

@ -24,19 +24,15 @@ def show_keymap(kb_info_json, title_caps=True):
keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
if keymap_path and keymap_path.suffix == '.json':
if title_caps:
cli.echo('{fg_blue}Keymap "%s"{fg_reset}:', cli.config.info.keymap)
else:
cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
keymap_data = json.load(keymap_path.open(encoding='utf-8'))
layout_name = keymap_data['layout']
layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name) # Resolve alias names
for layer_num, layer in enumerate(keymap_data['layers']):
if title_caps:
cli.echo('{fg_cyan}Layer %s{fg_reset}:', layer_num)
cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num)
else:
cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num)
cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num)
print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer))
@ -45,7 +41,7 @@ def show_layouts(kb_info_json, title_caps=True):
"""Render the layouts with info.json labels.
"""
for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items():
title = layout_name.title() if title_caps else layout_name
title = f'Layout {layout_name.title()}' if title_caps else f'layouts.{layout_name}'
cli.echo('{fg_cyan}%s{fg_reset}:', title)
print(layout_art) # Avoid passing dirty data to cli.echo()
@ -93,15 +89,6 @@ def print_friendly_output(kb_info_json):
aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]
cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
if cli.config.info.layouts:
show_layouts(kb_info_json, True)
if cli.config.info.matrix:
show_matrix(kb_info_json, True)
if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
show_keymap(kb_info_json, True)
def print_text_output(kb_info_json):
"""Print the info.json in a plain text format.
@ -122,6 +109,24 @@ def print_text_output(kb_info_json):
show_keymap(kb_info_json, False)
def print_dotted_output(kb_info_json, prefix=''):
"""Print the info.json in a plain text format with dot-joined keys.
"""
for key in sorted(kb_info_json):
new_prefix = f'{prefix}.{key}' if prefix else key
if key in ['parse_errors', 'parse_warnings']:
continue
elif key == 'layouts' and prefix == '':
cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
elif isinstance(kb_info_json[key], dict):
print_dotted_output(kb_info_json[key], new_prefix)
elif isinstance(kb_info_json[key], list):
cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, ', '.join(map(str, sorted(kb_info_json[key]))))
else:
cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, kb_info_json[key])
def print_parsed_rules_mk(keyboard_name):
rules = rules_mk(keyboard_name)
for k in sorted(rules.keys()):
@ -162,10 +167,22 @@ def info(cli):
# Output in the requested format
if cli.args.format == 'json':
print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
return True
elif cli.args.format == 'text':
print_text_output(kb_info_json)
print_dotted_output(kb_info_json)
title_caps = False
elif cli.args.format == 'friendly':
print_friendly_output(kb_info_json)
title_caps = True
else:
cli.log.error('Unknown format: %s', cli.args.format)
return False
if cli.config.info.layouts:
show_layouts(kb_info_json, title_caps)
if cli.config.info.matrix:
show_matrix(kb_info_json, title_caps)
if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
show_keymap(kb_info_json, title_caps)

View file

@ -1,72 +1,129 @@
"""Command to look over a keyboard/keymap and check for common mistakes.
"""
from pathlib import Path
from milc import cli
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.keyboard import find_readme, keyboard_completer
from qmk.keyboard import keyboard_completer, list_keyboards
from qmk.keymap import locate_keymap
from qmk.path import is_keyboard, keyboard
@cli.argument('--strict', action='store_true', help='Treat warnings as errors.')
@cli.argument('-kb', '--keyboard', completer=keyboard_completer, help='The keyboard to check.')
@cli.argument('-km', '--keymap', help='The keymap to check.')
def keymap_check(kb, km):
"""Perform the keymap level checks.
"""
ok = True
keymap_path = locate_keymap(kb, km)
if not keymap_path:
ok = False
cli.log.error("%s: Can't find %s keymap.", kb, km)
return ok
def rules_mk_assignment_only(keyboard_path):
"""Check the keyboard-level rules.mk to ensure it only has assignments.
"""
current_path = Path()
errors = []
for path_part in keyboard_path.parts:
current_path = current_path / path_part
rules_mk = current_path / 'rules.mk'
if rules_mk.exists():
continuation = None
for i, line in enumerate(rules_mk.open()):
line = line.strip()
if '#' in line:
line = line[:line.index('#')]
if continuation:
line = continuation + line
continuation = None
if line:
if line[-1] == '\\':
continuation = line[:-1]
continue
if line and '=' not in line:
errors.append(f'Non-assignment code on line +{i} {rules_mk}: {line}')
return errors
@cli.argument('--strict', action='store_true', help='Treat warnings as errors')
@cli.argument('-kb', '--keyboard', completer=keyboard_completer, help='Comma separated list of keyboards to check')
@cli.argument('-km', '--keymap', help='The keymap to check')
@cli.argument('--all-kb', action='store_true', arg_only=True, help='Check all keyboards')
@cli.subcommand('Check keyboard and keymap for common mistakes.')
@automagic_keyboard
@automagic_keymap
def lint(cli):
"""Check keyboard and keymap for common mistakes.
"""
if not cli.config.lint.keyboard:
cli.log.error('Missing required argument: --keyboard')
failed = []
# Determine our keyboard list
if cli.args.all_kb:
if cli.args.keyboard:
cli.log.warning('Both --all-kb and --keyboard passed, --all-kb takes presidence.')
keyboard_list = list_keyboards()
elif not cli.config.lint.keyboard:
cli.log.error('Missing required arguments: --keyboard or --all-kb')
cli.print_help()
return False
else:
keyboard_list = cli.config.lint.keyboard.split(',')
if not is_keyboard(cli.config.lint.keyboard):
cli.log.error('No such keyboard: %s', cli.config.lint.keyboard)
return False
# Lint each keyboard
for kb in keyboard_list:
if not is_keyboard(kb):
cli.log.error('No such keyboard: %s', kb)
continue
# Gather data about the keyboard.
ok = True
keyboard_path = keyboard(cli.config.lint.keyboard)
keyboard_info = info_json(cli.config.lint.keyboard)
readme_path = find_readme(cli.config.lint.keyboard)
missing_readme_path = keyboard_path / 'readme.md'
# Gather data about the keyboard.
ok = True
keyboard_path = keyboard(kb)
keyboard_info = info_json(kb)
# Check for errors in the info.json
if keyboard_info['parse_errors']:
ok = False
cli.log.error('Errors found when generating info.json.')
if cli.config.lint.strict and keyboard_info['parse_warnings']:
ok = False
cli.log.error('Warnings found when generating info.json (Strict mode enabled.)')
# Check for a readme.md and warn if it doesn't exist
if not readme_path:
ok = False
cli.log.error('Missing %s', missing_readme_path)
# Keymap specific checks
if cli.config.lint.keymap:
keymap_path = locate_keymap(cli.config.lint.keyboard, cli.config.lint.keymap)
if not keymap_path:
# Check for errors in the info.json
if keyboard_info['parse_errors']:
ok = False
cli.log.error("Can't find %s keymap for %s keyboard.", cli.config.lint.keymap, cli.config.lint.keyboard)
else:
keymap_readme = keymap_path.parent / 'readme.md'
if not keymap_readme.exists():
cli.log.warning('Missing %s', keymap_readme)
cli.log.error('%s: Errors found when generating info.json.', kb)
if cli.config.lint.strict:
ok = False
if cli.config.lint.strict and keyboard_info['parse_warnings']:
ok = False
cli.log.error('%s: Warnings found when generating info.json (Strict mode enabled.)', kb)
# Check the rules.mk file(s)
rules_mk_assignment_errors = rules_mk_assignment_only(keyboard_path)
if rules_mk_assignment_errors:
ok = False
cli.log.error('%s: Non-assignment code found in rules.mk. Move it to post_rules.mk instead.', kb)
for assignment_error in rules_mk_assignment_errors:
cli.log.error(assignment_error)
# Keymap specific checks
if cli.config.lint.keymap:
if not keymap_check(kb, cli.config.lint.keymap):
ok = False
# Report status
if not ok:
failed.append(kb)
# Check and report the overall status
if ok:
cli.log.info('Lint check passed!')
return True
if failed:
cli.log.error('Lint check failed for: %s', ', '.join(failed))
return False
cli.log.error('Lint check failed!')
return False
cli.log.info('Lint check passed!')
return True

View file

@ -10,7 +10,7 @@ from subprocess import DEVNULL
from milc import cli
from qmk.constants import QMK_FIRMWARE
from qmk.commands import _find_make
from qmk.commands import _find_make, get_make_parallel_args
import qmk.keyboard
import qmk.keymap
@ -28,7 +28,7 @@ def _is_split(keyboard_name):
return True if 'SPLIT_KEYBOARD' in rules_mk and rules_mk['SPLIT_KEYBOARD'].lower() == 'yes' else False
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs to run.")
@cli.argument('-j', '--parallel', type=int, default=1, help="Set the number of parallel make jobs; 0 means unlimited.")
@cli.argument('-c', '--clean', arg_only=True, action='store_true', help="Remove object files before compiling.")
@cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on the supplied value in rules.mk. Supported format is 'SPLIT_KEYBOARD=yes'. May be passed multiple times.")
@cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.")
@ -80,7 +80,7 @@ all: {keyboard_safe}_binary
)
# yapf: enable
cli.run([make_cmd, '-j', str(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
cli.run([make_cmd, *get_make_parallel_args(cli.args.parallel), '-f', makefile.as_posix(), 'all'], capture_output=False, stdin=DEVNULL)
# Check for failures
failures = [f for f in builddir.glob(f'failed.log.{os.getpid()}.*')]

View file

@ -115,9 +115,9 @@ def find_user_name():
def copy_templates(keyboard_type, keyboard_path):
"""Copies the template files from quantum/template to the new keyboard directory.
"""Copies the template files from data/templates to the new keyboard directory.
"""
template_base_path = Path('quantum/template')
template_base_path = Path('data/templates')
keyboard_basename = keyboard_path.name
cli.log.info('Copying base template files...')

View file

@ -12,6 +12,6 @@ def pytest(cli):
"""Run several linting/testing commands.
"""
nose2 = cli.run(['nose2', '-v'], capture_output=False, stdin=DEVNULL)
flake8 = cli.run(['flake8', 'lib/python', 'bin/qmk'], capture_output=False, stdin=DEVNULL)
flake8 = cli.run(['flake8', 'lib/python'], capture_output=False, stdin=DEVNULL)
return flake8.returncode | nose2.returncode

View file

@ -52,7 +52,7 @@ def create_make_target(target, parallel=1, **env_vars):
for key, value in env_vars.items():
env.append(f'{key}={value}')
return [make_cmd, '-j', str(parallel), *env, target]
return [make_cmd, *get_make_parallel_args(parallel), *env, target]
def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
@ -115,6 +115,24 @@ def get_git_version(current_time=None, repo_dir='.', check_dir='.'):
return current_time
def get_make_parallel_args(parallel=1):
"""Returns the arguments for running the specified number of parallel jobs.
"""
parallel_args = []
if int(parallel) <= 0:
# 0 or -1 means -j without argument (unlimited jobs)
parallel_args.append('--jobs')
else:
parallel_args.append('--jobs=' + str(parallel))
if int(parallel) != 1:
# If more than 1 job is used, synchronize parallel output by target
parallel_args.append('--output-sync=target')
return parallel_args
def create_version_h(skip_git=False, skip_all=False):
"""Generate version.h contents
"""
@ -188,8 +206,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
make_command.append('-s')
make_command.extend([
'-j',
str(parallel),
*get_make_parallel_args(parallel),
'-r',
'-R',
'-f',
@ -219,7 +236,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
f'VERBOSE={verbose}',
f'COLOR={color}',
'SILENT=false',
f'QMK_BIN={"bin/qmk" if "DEPRECATED_BIN_QMK" in os.environ else "qmk"}',
'QMK_BIN="qmk"',
])
return make_command

View file

@ -49,7 +49,7 @@ def info_json(keyboard):
info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
# Populate layout data
layouts, aliases = _find_all_layouts(info_data, keyboard)
layouts, aliases = _search_keyboard_h(keyboard)
if aliases:
info_data['layout_aliases'] = aliases
@ -61,8 +61,8 @@ def info_json(keyboard):
# Merge in the data from info.json, config.h, and rules.mk
info_data = merge_info_jsons(keyboard, info_data)
info_data = _extract_config_h(info_data)
info_data = _extract_rules_mk(info_data)
info_data = _extract_config_h(info_data)
# Ensure that we have matrix row and column counts
info_data = _matrix_size(info_data)
@ -77,6 +77,9 @@ def info_json(keyboard):
exit(1)
# Make sure we have at least one layout
if not info_data.get('layouts'):
_find_missing_layouts(info_data, keyboard)
if not info_data.get('layouts'):
_log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
@ -158,10 +161,9 @@ def _extract_pins(pins):
return [_pin_name(pin) for pin in pins.split(',')]
def _extract_direct_matrix(info_data, direct_pins):
def _extract_direct_matrix(direct_pins):
"""
"""
info_data['matrix_pins'] = {}
direct_pin_array = []
while direct_pins[-1] != '}':
@ -185,12 +187,157 @@ def _extract_direct_matrix(info_data, direct_pins):
return direct_pin_array
def _extract_audio(info_data, config_c):
"""Populate data about the audio configuration
"""
audio_pins = []
for pin in 'B5', 'B6', 'B7', 'C4', 'C5', 'C6':
if config_c.get(f'{pin}_AUDIO'):
audio_pins.append(pin)
if audio_pins:
info_data['audio'] = {'pins': audio_pins}
def _extract_split_main(info_data, config_c):
"""Populate data about the split configuration
"""
# Figure out how the main half is determined
if config_c.get('SPLIT_HAND_PIN') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_PIN) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'pin'
if config_c.get('SPLIT_HAND_MATRIX_GRID'):
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_MATRIX_GRID) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'matrix_grid'
info_data['split']['matrix_grid'] = _extract_pins(config_c['SPLIT_HAND_MATRIX_GRID'])
if config_c.get('EE_HANDS') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (EE_HANDS) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'eeprom'
if config_c.get('MASTER_RIGHT') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_RIGHT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'right'
if config_c.get('MASTER_LEFT') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'main' in info_data['split']:
_log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_LEFT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
info_data['split']['main'] = 'left'
def _extract_split_transport(info_data, config_c):
# Figure out the transport method
if config_c.get('USE_I2C') is True:
if 'split' not in info_data:
info_data['split'] = {}
if 'transport' not in info_data['split']:
info_data['split']['transport'] = {}
if 'protocol' in info_data['split']['transport']:
_log_warning(info_data, 'Split transport is specified in both config.h (USE_I2C) and info.json (split.transport.protocol) (Value: %s), the config.h value wins.' % info_data['split']['transport'])
info_data['split']['transport']['protocol'] = 'i2c'
elif 'protocol' not in info_data.get('split', {}).get('transport', {}):
if 'split' not in info_data:
info_data['split'] = {}
if 'transport' not in info_data['split']:
info_data['split']['transport'] = {}
info_data['split']['transport']['protocol'] = 'serial'
def _extract_split_right_pins(info_data, config_c):
# Figure out the right half matrix pins
row_pins = config_c.get('MATRIX_ROW_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
col_pins = config_c.get('MATRIX_COL_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
unused_pin_text = config_c.get('UNUSED_PINS_RIGHT')
unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None
direct_pins = config_c.get('DIRECT_PINS_RIGHT', '').replace(' ', '')[1:-1]
if row_pins and col_pins:
if info_data.get('split', {}).get('matrix_pins', {}).get('right') in info_data:
_log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
if 'split' not in info_data:
info_data['split'] = {}
if 'matrix_pins' not in info_data['split']:
info_data['split']['matrix_pins'] = {}
if 'right' not in info_data['split']['matrix_pins']:
info_data['split']['matrix_pins']['right'] = {}
info_data['split']['matrix_pins']['right'] = {
'cols': _extract_pins(col_pins),
'rows': _extract_pins(row_pins),
}
if direct_pins:
if info_data.get('split', {}).get('matrix_pins', {}).get('right', {}):
_log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
if 'split' not in info_data:
info_data['split'] = {}
if 'matrix_pins' not in info_data['split']:
info_data['split']['matrix_pins'] = {}
if 'right' not in info_data['split']['matrix_pins']:
info_data['split']['matrix_pins']['right'] = {}
info_data['split']['matrix_pins']['right']['direct'] = _extract_direct_matrix(direct_pins)
if unused_pins:
if 'split' not in info_data:
info_data['split'] = {}
if 'matrix_pins' not in info_data['split']:
info_data['split']['matrix_pins'] = {}
if 'right' not in info_data['split']['matrix_pins']:
info_data['split']['matrix_pins']['right'] = {}
info_data['split']['matrix_pins']['right']['unused'] = _extract_pins(unused_pins)
def _extract_matrix_info(info_data, config_c):
"""Populate the matrix information.
"""
row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
unused_pin_text = config_c.get('UNUSED_PINS')
unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None
direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
info_snippet = {}
if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
if 'matrix_size' in info_data:
@ -202,19 +349,35 @@ def _extract_matrix_info(info_data, config_c):
}
if row_pins and col_pins:
if 'matrix_pins' in info_data:
if 'matrix_pins' in info_data and 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
_log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
info_data['matrix_pins'] = {
'cols': _extract_pins(col_pins),
'rows': _extract_pins(row_pins),
}
info_snippet['cols'] = _extract_pins(col_pins)
info_snippet['rows'] = _extract_pins(row_pins)
if direct_pins:
if 'matrix_pins' in info_data:
if 'matrix_pins' in info_data and 'direct' in info_data['matrix_pins']:
_log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
info_snippet['direct'] = _extract_direct_matrix(direct_pins)
if unused_pins:
if 'matrix_pins' not in info_data:
info_data['matrix_pins'] = {}
info_snippet['unused'] = _extract_pins(unused_pins)
if config_c.get('CUSTOM_MATRIX', 'no') != 'no':
if 'matrix_pins' in info_data and 'custom' in info_data['matrix_pins']:
_log_warning(info_data, 'Custom Matrix is specified in both info.json and config.h, the config.h values win.')
info_snippet['custom'] = True
if config_c['CUSTOM_MATRIX'] == 'lite':
info_snippet['custom_lite'] = True
if info_snippet:
info_data['matrix_pins'] = info_snippet
return info_data
@ -272,6 +435,10 @@ def _extract_config_h(info_data):
# Pull data that easily can't be mapped in json
_extract_matrix_info(info_data, config_c)
_extract_audio(info_data, config_c)
_extract_split_main(info_data, config_c)
_extract_split_transport(info_data, config_c)
_extract_split_right_pins(info_data, config_c)
return info_data
@ -384,45 +551,13 @@ def _check_matrix(info_data):
_log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}')
def _merge_layouts(info_data, new_info_data):
"""Merge new_info_data into info_data in an intelligent way.
"""
for layout_name, layout_json in new_info_data['layouts'].items():
if layout_name in info_data['layouts']:
# Pull in layouts we have a macro for
if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
_log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
else:
for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
key.update(layout_json['layout'][i])
else:
# Pull in layouts that have matrix data
missing_matrix = False
for key in layout_json.get('layout', {}):
if 'matrix' not in key:
missing_matrix = True
if not missing_matrix:
if layout_name in info_data['layouts']:
# Update an existing layout with new data
for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
key.update(layout_json['layout'][i])
else:
# Copy in the new layout wholesale
layout_json['c_macro'] = False
info_data['layouts'][layout_name] = layout_json
return info_data
def _search_keyboard_h(path):
def _search_keyboard_h(keyboard):
keyboard = Path(keyboard)
current_path = Path('keyboards/')
aliases = {}
layouts = {}
for directory in path.parts:
for directory in keyboard.parts:
current_path = current_path / directory
keyboard_h = '%s.h' % (directory,)
keyboard_h_path = current_path / keyboard_h
@ -437,27 +572,28 @@ def _search_keyboard_h(path):
return layouts, aliases
def _find_all_layouts(info_data, keyboard):
"""Looks for layout macros associated with this keyboard.
def _find_missing_layouts(info_data, keyboard):
"""Looks for layout macros when they aren't found other places.
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.
"""
layouts, aliases = _search_keyboard_h(Path(keyboard))
_log_warning(info_data, '%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
if not layouts:
# 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.
info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
for file in glob('keyboards/%s/*.h' % keyboard):
these_layouts, these_aliases = find_layouts(file)
for file in glob('keyboards/%s/*.h' % keyboard):
if file.endswith('.h'):
these_layouts, these_aliases = find_layouts(file)
if these_layouts:
for layout_name, layout_json in these_layouts.items():
if not layout_name.startswith('LAYOUT_kc'):
layout_json['c_macro'] = True
info_data['layouts'][layout_name] = layout_json
if these_layouts:
layouts.update(these_layouts)
for alias, alias_text in these_aliases.items():
if alias_text in these_layouts:
if 'layout_aliases' not in info_data:
info_data['layout_aliases'] = {}
for alias, alias_text in these_aliases.items():
if alias_text in layouts:
aliases[alias] = alias_text
return layouts, aliases
info_data['layout_aliases'][alias] = alias_text
def _log_error(info_data, message):
@ -554,8 +690,12 @@ def merge_info_jsons(keyboard, info_data):
layout_name = info_data['layout_aliases'][layout_name]
if layout_name in info_data['layouts']:
for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
existing_key.update(new_key)
if len(info_data['layouts'][layout_name]['layout']) != len(layout['layout']):
msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
_log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
else:
for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
existing_key.update(new_key)
else:
layout['c_macro'] = False
info_data['layouts'][layout_name] = layout

View file

@ -2,6 +2,7 @@
"""
import json
from collections.abc import Mapping
from functools import lru_cache
from pathlib import Path
import hjson
@ -25,6 +26,7 @@ def json_load(json_file):
exit(1)
@lru_cache(maxsize=0)
def load_jsonschema(schema_name):
"""Read a jsonschema file from disk.
"""
@ -39,8 +41,9 @@ def load_jsonschema(schema_name):
return json_load(schema_path)
def create_validator(schema):
"""Creates a validator for the given schema id.
@lru_cache(maxsize=0)
def compile_schema_store():
"""Compile all our schemas into a schema store.
"""
schema_store = {}
@ -51,6 +54,14 @@ def create_validator(schema):
continue
schema_store[schema_data['$id']] = schema_data
return schema_store
@lru_cache(maxsize=0)
def create_validator(schema):
"""Creates a validator for the given schema id.
"""
schema_store = compile_schema_store()
resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate

View file

@ -83,7 +83,7 @@ def test_hello():
def test_format_python():
result = check_subcommand('format-python', '--dry-run')
check_returncode(result)
assert 'Python code in `bin/qmk` and `lib/python` is correctly formatted.' in result.stdout
assert 'Python code in `lib/python` is correctly formatted.' in result.stdout
def test_list_keyboards():