2020-05-26 13:05:41 -07:00
"""Functions that help us generate and use info.json files.
"""
2023-02-11 20:36:11 +00:00
import re
2020-05-26 13:05:41 -07:00
from pathlib import Path
2020-12-01 12:52:02 -08:00
import jsonschema
2021-01-31 12:46:00 -08:00
from dotty_dict import dotty
2023-02-11 20:36:11 +00:00
2020-05-26 13:05:41 -07:00
from milc import cli
2021-01-31 12:46:00 -08:00
from qmk.constants import CHIBIOS_PROCESSORS , LUFA_PROCESSORS , VUSB_PROCESSORS
2022-05-15 22:39:29 +01:00
from qmk.c_parse import find_layouts , parse_config_h_file , find_led_config
2021-06-24 20:48:53 -07:00
from qmk.json_schema import deep_update , json_load , validate
2020-05-26 13:05:41 -07:00
from qmk.keyboard import config_h , rules_mk
2022-03-21 10:57:41 +00:00
from qmk.commands import parse_configurator_json
2020-10-07 10:57:40 +11:00
from qmk.makefile import parse_rules_mk_file
2020-05-26 13:05:41 -07:00
from qmk.math import compute
2020-12-30 10:27:37 -08:00
true_values = [ '1' , 'on' , 'yes' ]
false_values = [ '0' , 'off' , 'no' ]
2023-02-11 20:36:11 +00:00
def _keyboard_in_layout_name ( keyboard , layout ):
"""Validate that a layout macro does not contain name of keyboard
"""
# TODO: reduce this list down
safe_layout_tokens = {
'ansi' ,
'iso' ,
'jp' ,
'jis' ,
'ortho' ,
'wkl' ,
'tkl' ,
'preonic' ,
'planck' ,
}
# Ignore tokens like 'split_3x7_4' or just '2x4'
layout = re . sub ( r "_split_\d+x\d+_\d+" , '' , layout )
layout = re . sub ( r "_\d+x\d+" , '' , layout )
name_fragments = set ( keyboard . split ( '/' )) - safe_layout_tokens
return any ( fragment in layout for fragment in name_fragments )
2022-11-30 06:39:41 +00:00
2020-05-26 13:05:41 -07:00
2021-05-09 21:39:48 +01:00
def _valid_community_layout ( layout ):
"""Validate that a declared community list exists
"""
return ( Path ( 'layouts/default' ) / layout ) . exists ()
2022-11-30 04:27:48 +00:00
def _validate ( keyboard , info_data ):
"""Perform various validation on the provided info.json data
"""
# First validate against the jsonschema
try :
validate ( info_data , 'qmk.api.keyboard.v1' )
except jsonschema . ValidationError as e :
json_path = '.' . join ([ str ( p ) for p in e . absolute_path ])
cli . log . error ( 'Invalid API data: %s : %s : %s ' , keyboard , json_path , e . message )
exit ( 1 )
layouts = info_data . get ( 'layouts' , {})
layout_aliases = info_data . get ( 'layout_aliases' , {})
community_layouts = info_data . get ( 'community_layouts' , [])
2022-11-30 06:39:41 +00:00
community_layouts_names = list ( map ( lambda layout : f 'LAYOUT_ { layout } ' , community_layouts ))
2022-11-30 04:27:48 +00:00
# Make sure we have at least one layout
2023-01-19 00:27:11 +00:00
if len ( layouts ) == 0 or all ( not layout . get ( 'json_layout' , False ) for layout in layouts . values ()):
2022-11-30 04:27:48 +00:00
_log_error ( info_data , 'No LAYOUTs defined! Need at least one layout defined in info.json.' )
# Providing only LAYOUT_all "because I define my layouts in a 3rd party tool"
if len ( layouts ) == 1 and 'LAYOUT_all' in layouts :
_log_warning ( info_data , '"LAYOUT_all" should be "LAYOUT" unless additional layouts are provided.' )
2022-11-30 06:39:41 +00:00
# Extended layout name checks - ignoring community_layouts and "safe" values
potential_layouts = set ( layouts . keys ()) - set ( community_layouts_names )
for layout in potential_layouts :
2023-02-11 20:36:11 +00:00
if _keyboard_in_layout_name ( keyboard , layout ):
2022-11-30 04:27:48 +00:00
_log_warning ( info_data , f 'Layout " { layout } " should not contain name of keyboard.' )
# Filter out any non-existing community layouts
for layout in community_layouts :
if not _valid_community_layout ( layout ):
# Ignore layout from future checks
info_data [ 'community_layouts' ] . remove ( layout )
_log_error ( info_data , 'Claims to support a community layout that does not exist: %s ' % ( layout ))
# Make sure we supply layout macros for the community layouts we claim to support
2022-11-30 06:39:41 +00:00
for layout_name in community_layouts_names :
2022-11-30 04:27:48 +00:00
if layout_name not in layouts and layout_name not in layout_aliases :
_log_error ( info_data , 'Claims to support community layout %s but no %s () macro found' % ( layout , layout_name ))
2020-05-26 13:05:41 -07:00
def info_json ( keyboard ):
"""Generate the info.json data for a specific keyboard.
"""
2020-10-07 10:57:40 +11:00
cur_dir = Path ( 'keyboards' )
2021-09-27 10:02:54 -07:00
root_rules_mk = parse_rules_mk_file ( cur_dir / keyboard / 'rules.mk' )
if 'DEFAULT_FOLDER' in root_rules_mk :
keyboard = root_rules_mk [ 'DEFAULT_FOLDER' ]
2020-10-07 10:57:40 +11:00
2020-05-26 13:05:41 -07:00
info_data = {
'keyboard_name' : str ( keyboard ),
'keyboard_folder' : str ( keyboard ),
2020-10-25 14:48:44 -07:00
'keymaps' : {},
2020-05-26 13:05:41 -07:00
'layouts' : {},
2020-11-07 09:56:08 -08:00
'parse_errors' : [],
'parse_warnings' : [],
2020-05-26 13:05:41 -07:00
'maintainer' : 'qmk' ,
}
2020-10-25 14:48:44 -07:00
# Populate layout data
2021-08-15 13:30:58 -07:00
layouts , aliases = _search_keyboard_h ( keyboard )
2021-02-27 12:00:50 -08:00
if aliases :
info_data [ 'layout_aliases' ] = aliases
for layout_name , layout_json in layouts . items ():
2020-05-26 13:05:41 -07:00
if not layout_name . startswith ( 'LAYOUT_kc' ):
2020-12-30 10:27:37 -08:00
layout_json [ 'c_macro' ] = True
2023-01-19 00:27:11 +00:00
layout_json [ 'json_layout' ] = False
2020-05-26 13:05:41 -07:00
info_data [ 'layouts' ][ layout_name ] = layout_json
2020-10-25 14:48:44 -07:00
# Merge in the data from info.json, config.h, and rules.mk
2020-05-26 13:05:41 -07:00
info_data = merge_info_jsons ( keyboard , info_data )
2022-04-03 18:45:10 +01:00
info_data = _process_defaults ( info_data )
2022-03-21 10:57:41 +00:00
info_data = _extract_rules_mk ( info_data , rules_mk ( str ( keyboard )))
info_data = _extract_config_h ( info_data , config_h ( str ( keyboard )))
2020-05-26 13:05:41 -07:00
2021-07-11 09:43:58 -07:00
# Ensure that we have matrix row and column counts
info_data = _matrix_size ( info_data )
2022-05-15 22:39:29 +01:00
# Merge in data from <keyboard.c>
info_data = _extract_led_config ( info_data , str ( keyboard ))
2022-11-30 04:27:48 +00:00
# Validate
_validate ( keyboard , info_data )
2020-12-30 10:27:37 -08:00
2021-07-11 09:43:58 -07:00
# Check that the reported matrix size is consistent with the actual matrix size
_check_matrix ( info_data )
2020-05-26 13:05:41 -07:00
return info_data
2020-12-30 10:27:37 -08:00
def _extract_features ( info_data , rules ):
"""Find all the features enabled in rules.mk.
"""
2022-08-19 01:56:48 +01:00
# Process booleans rules
2020-12-30 10:27:37 -08:00
for key , value in rules . items ():
if key . endswith ( '_ENABLE' ):
key = '_' . join ( key . split ( '_' )[: - 1 ]) . lower ()
2020-12-01 16:04:22 -08:00
value = True if value . lower () in true_values else False if value . lower () in false_values else value
2020-12-30 10:27:37 -08:00
if 'config_h_features' not in info_data :
info_data [ 'config_h_features' ] = {}
if 'features' not in info_data :
info_data [ 'features' ] = {}
if key in info_data [ 'features' ]:
_log_warning ( info_data , 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % ( key ,))
info_data [ 'features' ][ key ] = value
info_data [ 'config_h_features' ][ key ] = value
return info_data
2021-01-08 00:00:15 -08:00
def _pin_name ( pin ):
"""Returns the proper representation for a pin.
"""
pin = pin . strip ()
if not pin :
return None
elif pin . isdigit ():
return int ( pin )
elif pin == 'NO_PIN' :
return None
2021-06-22 12:02:12 -07:00
return pin
2021-01-08 00:00:15 -08:00
2020-12-30 11:21:18 -08:00
def _extract_pins ( pins ):
"""Returns a list of pins from a comma separated string of pins.
"""
2021-01-08 00:00:15 -08:00
return [ _pin_name ( pin ) for pin in pins . split ( ',' )]
2020-12-30 11:21:18 -08:00
2022-04-16 19:13:05 +01:00
def _extract_2d_array ( raw ):
"""Return a 2d array of strings
2020-12-30 11:21:18 -08:00
"""
2022-04-16 19:13:05 +01:00
out_array = []
2020-12-30 11:21:18 -08:00
2022-04-16 19:13:05 +01:00
while raw [ - 1 ] != '}' :
raw = raw [: - 1 ]
2020-12-30 11:21:18 -08:00
2022-04-16 19:13:05 +01:00
for row in raw . split ( '},{' ):
2020-12-30 11:21:18 -08:00
if row . startswith ( '{' ):
row = row [ 1 :]
if row . endswith ( '}' ):
row = row [: - 1 ]
2022-04-16 19:13:05 +01:00
out_array . append ([])
2020-12-30 11:21:18 -08:00
2022-04-16 19:13:05 +01:00
for val in row . split ( ',' ):
out_array [ - 1 ] . append ( val )
2020-12-30 11:21:18 -08:00
2022-04-16 19:13:05 +01:00
return out_array
def _extract_2d_int_array ( raw ):
"""Return a 2d array of ints
"""
ret = _extract_2d_array ( raw )
return [ list ( map ( int , x )) for x in ret ]
def _extract_direct_matrix ( direct_pins ):
"""extract direct_matrix
"""
direct_pin_array = _extract_2d_array ( direct_pins )
for i in range ( len ( direct_pin_array )):
for j in range ( len ( direct_pin_array [ i ])):
if direct_pin_array [ i ][ j ] == 'NO_PIN' :
direct_pin_array [ i ][ j ] = None
2020-12-30 11:21:18 -08:00
return direct_pin_array
2021-08-16 15:33:30 -07:00
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 }
2022-06-21 04:15:06 +01:00
def _extract_encoders_values ( config_c , postfix = '' ):
"""Common encoder extraction logic
"""
a_pad = config_c . get ( f 'ENCODERS_PAD_A { postfix } ' , '' ) . replace ( ' ' , '' )[ 1 : - 1 ]
b_pad = config_c . get ( f 'ENCODERS_PAD_B { postfix } ' , '' ) . replace ( ' ' , '' )[ 1 : - 1 ]
resolutions = config_c . get ( f 'ENCODER_RESOLUTIONS { postfix } ' , '' ) . replace ( ' ' , '' )[ 1 : - 1 ]
2022-07-11 10:51:39 +01:00
default_resolution = config_c . get ( 'ENCODER_RESOLUTION' , None )
2022-06-21 04:15:06 +01:00
if a_pad and b_pad :
a_pad = list ( filter ( None , a_pad . split ( ',' )))
b_pad = list ( filter ( None , b_pad . split ( ',' )))
resolutions = list ( filter ( None , resolutions . split ( ',' )))
2022-07-11 10:51:39 +01:00
if default_resolution :
resolutions += [ default_resolution ] * ( len ( a_pad ) - len ( resolutions ))
2022-06-21 04:15:06 +01:00
encoders = []
for index in range ( len ( a_pad )):
2022-07-11 10:51:39 +01:00
encoder = { 'pin_a' : a_pad [ index ], 'pin_b' : b_pad [ index ]}
if index < len ( resolutions ):
encoder [ 'resolution' ] = int ( resolutions [ index ])
encoders . append ( encoder )
2022-06-21 04:15:06 +01:00
return encoders
def _extract_encoders ( info_data , config_c ):
"""Populate data about encoder pins
"""
encoders = _extract_encoders_values ( config_c )
if encoders :
if 'encoder' not in info_data :
info_data [ 'encoder' ] = {}
if 'rotary' in info_data [ 'encoder' ]:
_log_warning ( info_data , 'Encoder config is specified in both config.h and info.json (encoder.rotary) (Value: %s ), the config.h value wins.' % info_data [ 'encoder' ][ 'rotary' ])
info_data [ 'encoder' ][ 'rotary' ] = encoders
def _extract_split_encoders ( info_data , config_c ):
"""Populate data about split encoder pins
"""
encoders = _extract_encoders_values ( config_c , '_RIGHT' )
if encoders :
if 'split' not in info_data :
info_data [ 'split' ] = {}
if 'encoder' not in info_data [ 'split' ]:
info_data [ 'split' ][ 'encoder' ] = {}
if 'right' not in info_data [ 'split' ][ 'encoder' ]:
info_data [ 'split' ][ 'encoder' ][ 'right' ] = {}
if 'rotary' in info_data [ 'split' ][ 'encoder' ][ 'right' ]:
_log_warning ( info_data , 'Encoder config is specified in both config.h and info.json (encoder.rotary) (Value: %s ), the config.h value wins.' % info_data [ 'split' ][ 'encoder' ][ 'right' ][ 'rotary' ])
info_data [ 'split' ][ 'encoder' ][ 'right' ][ 'rotary' ] = encoders
2022-04-16 19:13:05 +01:00
def _extract_secure_unlock ( info_data , config_c ):
"""Populate data about the secure unlock sequence
"""
unlock = config_c . get ( 'SECURE_UNLOCK_SEQUENCE' , '' ) . replace ( ' ' , '' )[ 1 : - 1 ]
if unlock :
unlock_array = _extract_2d_int_array ( unlock )
if 'secure' not in info_data :
info_data [ 'secure' ] = {}
if 'unlock_sequence' in info_data [ 'secure' ]:
_log_warning ( info_data , 'Secure unlock sequence is specified in both config.h (SECURE_UNLOCK_SEQUENCE) and info.json (secure.unlock_sequence) (Value: %s ), the config.h value wins.' % info_data [ 'secure' ][ 'unlock_sequence' ])
info_data [ 'secure' ][ 'unlock_sequence' ] = unlock_array
2021-08-16 15:33:30 -07:00
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'
2022-03-22 10:48:13 +00:00
# Ignore transport defaults if "SPLIT_KEYBOARD" is unset
elif 'enabled' in info_data . get ( 'split' , {}):
2021-08-16 15:33:30 -07:00
if 'split' not in info_data :
info_data [ 'split' ] = {}
if 'transport' not in info_data [ 'split' ]:
info_data [ 'split' ][ 'transport' ] = {}
2022-03-22 10:48:13 +00:00
if 'protocol' not in info_data [ 'split' ][ 'transport' ]:
info_data [ 'split' ][ 'transport' ][ 'protocol' ] = 'serial'
2021-08-16 15:33:30 -07:00
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 ()
direct_pins = config_c . get ( 'DIRECT_PINS_RIGHT' , '' ) . replace ( ' ' , '' )[ 1 : - 1 ]
2022-08-06 23:23:35 +10:00
if row_pins or col_pins or direct_pins :
2022-09-09 21:22:55 +01:00
if info_data . get ( 'split' , {}) . get ( 'matrix_pins' , {}) . get ( 'right' , None ):
2021-08-16 15:33:30 -07:00
_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' ] = {}
2022-07-08 22:49:55 +01:00
if col_pins :
info_data [ 'split' ][ 'matrix_pins' ][ 'right' ][ 'cols' ] = _extract_pins ( col_pins )
2021-08-16 15:33:30 -07:00
2022-07-08 22:49:55 +01:00
if row_pins :
info_data [ 'split' ][ 'matrix_pins' ][ 'right' ][ 'rows' ] = _extract_pins ( row_pins )
2021-08-16 15:33:30 -07:00
2022-07-08 22:49:55 +01:00
if direct_pins :
info_data [ 'split' ][ 'matrix_pins' ][ 'right' ][ 'direct' ] = _extract_direct_matrix ( direct_pins )
2021-08-16 15:33:30 -07:00
2020-12-30 10:27:37 -08:00
def _extract_matrix_info ( info_data , config_c ):
"""Populate the matrix information.
2020-05-26 13:05:41 -07:00
"""
row_pins = config_c . get ( 'MATRIX_ROW_PINS' , '' ) . replace ( '{' , '' ) . replace ( '}' , '' ) . strip ()
col_pins = config_c . get ( 'MATRIX_COL_PINS' , '' ) . replace ( '{' , '' ) . replace ( '}' , '' ) . strip ()
direct_pins = config_c . get ( 'DIRECT_PINS' , '' ) . replace ( ' ' , '' )[ 1 : - 1 ]
2021-08-16 15:33:30 -07:00
info_snippet = {}
2020-05-26 13:05:41 -07:00
2020-12-30 10:27:37 -08:00
if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c :
if 'matrix_size' in info_data :
_log_warning ( info_data , 'Matrix size is specified in both info.json and config.h, the config.h values win.' )
2020-05-26 13:05:41 -07:00
2020-12-30 10:27:37 -08:00
info_data [ 'matrix_size' ] = {
'cols' : compute ( config_c . get ( 'MATRIX_COLS' , '0' )),
2020-12-30 11:21:18 -08:00
'rows' : compute ( config_c . get ( 'MATRIX_ROWS' , '0' )),
2020-12-30 10:27:37 -08:00
}
if row_pins and col_pins :
2021-08-16 15:33:30 -07:00
if 'matrix_pins' in info_data and 'cols' in info_data [ 'matrix_pins' ] and 'rows' in info_data [ 'matrix_pins' ]:
2020-12-30 10:27:37 -08:00
_log_warning ( info_data , 'Matrix pins are specified in both info.json and config.h, the config.h values win.' )
2021-08-16 15:33:30 -07:00
info_snippet [ 'cols' ] = _extract_pins ( col_pins )
info_snippet [ 'rows' ] = _extract_pins ( row_pins )
2020-05-26 13:05:41 -07:00
if direct_pins :
2021-08-16 15:33:30 -07:00
if 'matrix_pins' in info_data and 'direct' in info_data [ 'matrix_pins' ]:
2020-12-30 10:27:37 -08:00
_log_warning ( info_data , 'Direct pins are specified in both info.json and config.h, the config.h values win.' )
2021-08-16 15:33:30 -07:00
info_snippet [ 'direct' ] = _extract_direct_matrix ( direct_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
2020-05-26 13:05:41 -07:00
2020-12-30 10:27:37 -08:00
return info_data
2022-06-09 21:02:16 +01:00
def _config_to_json ( key_type , config_value ):
"""Convert config value using spec
"""
if key_type . startswith ( 'array' ):
if '.' in key_type :
key_type , array_type = key_type . split ( '.' , 1 )
else :
array_type = None
config_value = config_value . replace ( '{' , '' ) . replace ( '}' , '' ) . strip ()
if array_type == 'int' :
return list ( map ( int , config_value . split ( ',' )))
else :
2022-11-23 18:48:22 +00:00
return list ( map ( str . strip , config_value . split ( ',' )))
2022-06-09 21:02:16 +01:00
elif key_type == 'bool' :
return config_value in true_values
elif key_type == 'hex' :
return '0x' + config_value [ 2 :] . upper ()
elif key_type == 'list' :
return config_value . split ()
elif key_type == 'int' :
return int ( config_value )
elif key_type == 'str' :
2022-08-29 04:35:17 +10:00
return config_value . strip ( '"' ) . replace ( ' \\ "' , '"' ) . replace ( ' \\\\ ' , ' \\ ' )
2022-06-09 21:02:16 +01:00
elif key_type == 'bcd_version' :
major = int ( config_value [ 2 : 4 ])
minor = int ( config_value [ 4 ])
revision = int ( config_value [ 5 ])
return f ' { major } . { minor } . { revision } '
return config_value
2022-03-21 10:57:41 +00:00
def _extract_config_h ( info_data , config_c ):
2020-12-30 10:27:37 -08:00
"""Pull some keyboard information from existing config.h files
"""
2021-01-31 12:46:00 -08:00
# Pull in data from the json map
dotty_info = dotty ( info_data )
2022-11-08 12:05:08 +11:00
info_config_map = json_load ( Path ( 'data/mappings/info_config.hjson' ))
2021-01-31 12:46:00 -08:00
for config_key , info_dict in info_config_map . items ():
info_key = info_dict [ 'info_key' ]
2022-03-05 00:25:24 +11:00
key_type = info_dict . get ( 'value_type' , 'raw' )
2021-01-31 12:46:00 -08:00
try :
2022-08-06 22:37:40 +10:00
replace_with = info_dict . get ( 'replace_with' )
2022-06-09 21:02:16 +01:00
if config_key in config_c and info_dict . get ( 'invalid' , False ):
2022-08-06 22:37:40 +10:00
if replace_with :
_log_error ( info_data , ' %s in config.h is no longer a valid option and should be replaced with %s ' % ( config_key , replace_with ))
else :
_log_error ( info_data , ' %s in config.h is no longer a valid option and should be removed' % config_key )
2022-06-09 21:02:16 +01:00
elif config_key in config_c and info_dict . get ( 'deprecated' , False ):
2022-08-06 22:37:40 +10:00
if replace_with :
_log_warning ( info_data , ' %s in config.h is deprecated in favor of %s and will be removed at a later date' % ( config_key , replace_with ))
else :
_log_warning ( info_data , ' %s in config.h is deprecated and will be removed at a later date' % config_key )
2022-06-09 21:02:16 +01:00
2021-01-31 12:46:00 -08:00
if config_key in config_c and info_dict . get ( 'to_json' , True ):
if dotty_info . get ( info_key ) and info_dict . get ( 'warn_duplicate' , True ):
_log_warning ( info_data , ' %s in config.h is overwriting %s in info.json' % ( config_key , info_key ))
2022-06-09 21:02:16 +01:00
dotty_info [ info_key ] = _config_to_json ( key_type , config_c [ config_key ])
2021-01-31 12:46:00 -08:00
except Exception as e :
_log_warning ( info_data , f ' { config_key } -> { info_key } : { e } ' )
info_data . update ( dotty_info )
# Pull data that easily can't be mapped in json
2020-12-30 10:27:37 -08:00
_extract_matrix_info ( info_data , config_c )
2021-08-16 15:33:30 -07:00
_extract_audio ( info_data , config_c )
2022-04-16 19:13:05 +01:00
_extract_secure_unlock ( info_data , config_c )
2021-08-16 15:33:30 -07:00
_extract_split_main ( info_data , config_c )
_extract_split_transport ( info_data , config_c )
_extract_split_right_pins ( info_data , config_c )
2022-06-21 04:15:06 +01:00
_extract_encoders ( info_data , config_c )
_extract_split_encoders ( info_data , config_c )
2020-05-26 13:05:41 -07:00
return info_data
2022-04-03 18:45:10 +01:00
def _process_defaults ( info_data ):
"""Process any additional defaults based on currently discovered information
"""
2022-11-08 12:05:08 +11:00
defaults_map = json_load ( Path ( 'data/mappings/defaults.hjson' ))
2022-04-03 18:45:10 +01:00
for default_type in defaults_map . keys ():
thing_map = defaults_map [ default_type ]
if default_type in info_data :
2023-01-07 17:05:53 +00:00
merged_count = 0
thing_items = thing_map . get ( info_data [ default_type ], {}) . items ()
for key , value in thing_items :
if key not in info_data :
info_data [ key ] = value
merged_count += 1
if merged_count == 0 and len ( thing_items ) > 0 :
_log_warning ( info_data , 'All defaults for \' %s \' were skipped, potential redundant config or misconfiguration detected' % ( default_type ))
2022-04-03 18:45:10 +01:00
return info_data
2022-03-21 10:57:41 +00:00
def _extract_rules_mk ( info_data , rules ):
2020-05-26 13:05:41 -07:00
"""Pull some keyboard information from existing rules.mk files
"""
2021-01-31 12:46:00 -08:00
info_data [ 'processor' ] = rules . get ( 'MCU' , info_data . get ( 'processor' , 'atmega32u4' ))
2020-05-26 13:05:41 -07:00
2021-01-31 12:46:00 -08:00
if info_data [ 'processor' ] in CHIBIOS_PROCESSORS :
2020-12-30 10:27:37 -08:00
arm_processor_rules ( info_data , rules )
2020-05-26 13:05:41 -07:00
2021-01-31 12:46:00 -08:00
elif info_data [ 'processor' ] in LUFA_PROCESSORS + VUSB_PROCESSORS :
2020-12-30 10:27:37 -08:00
avr_processor_rules ( info_data , rules )
2020-11-07 09:56:08 -08:00
2020-12-30 10:27:37 -08:00
else :
2021-01-31 12:46:00 -08:00
cli . log . warning ( " %s : Unknown MCU: %s " % ( info_data [ 'keyboard_folder' ], info_data [ 'processor' ]))
2020-12-30 10:27:37 -08:00
unknown_processor_rules ( info_data , rules )
2020-11-07 09:56:08 -08:00
2021-01-31 12:46:00 -08:00
# Pull in data from the json map
dotty_info = dotty ( info_data )
2022-11-08 12:05:08 +11:00
info_rules_map = json_load ( Path ( 'data/mappings/info_rules.hjson' ))
2021-01-31 12:46:00 -08:00
for rules_key , info_dict in info_rules_map . items ():
info_key = info_dict [ 'info_key' ]
2022-03-05 00:25:24 +11:00
key_type = info_dict . get ( 'value_type' , 'raw' )
2021-01-31 12:46:00 -08:00
try :
2022-08-06 22:37:40 +10:00
replace_with = info_dict . get ( 'replace_with' )
2022-06-09 21:02:16 +01:00
if rules_key in rules and info_dict . get ( 'invalid' , False ):
2022-08-06 22:37:40 +10:00
if replace_with :
_log_error ( info_data , ' %s in rules.mk is no longer a valid option and should be replaced with %s ' % ( rules_key , replace_with ))
else :
_log_error ( info_data , ' %s in rules.mk is no longer a valid option and should be removed' % rules_key )
2022-06-09 21:02:16 +01:00
elif rules_key in rules and info_dict . get ( 'deprecated' , False ):
2022-08-06 22:37:40 +10:00
if replace_with :
_log_warning ( info_data , ' %s in rules.mk is deprecated in favor of %s and will be removed at a later date' % ( rules_key , replace_with ))
else :
_log_warning ( info_data , ' %s in rules.mk is deprecated and will be removed at a later date' % rules_key )
2022-06-09 21:02:16 +01:00
2021-01-31 12:46:00 -08:00
if rules_key in rules and info_dict . get ( 'to_json' , True ):
if dotty_info . get ( info_key ) and info_dict . get ( 'warn_duplicate' , True ):
_log_warning ( info_data , ' %s in rules.mk is overwriting %s in info.json' % ( rules_key , info_key ))
2022-06-09 21:02:16 +01:00
dotty_info [ info_key ] = _config_to_json ( key_type , rules [ rules_key ])
2021-01-31 12:46:00 -08:00
except Exception as e :
_log_warning ( info_data , f ' { rules_key } -> { info_key } : { e } ' )
info_data . update ( dotty_info )
# Merge in config values that can't be easily mapped
2020-12-30 10:27:37 -08:00
_extract_features ( info_data , rules )
2020-11-07 09:56:08 -08:00
2020-12-30 10:27:37 -08:00
return info_data
2022-05-15 22:39:29 +01:00
def find_keyboard_c ( keyboard ):
"""Find all <keyboard>.c files
"""
keyboard = Path ( keyboard )
current_path = Path ( 'keyboards/' )
files = []
for directory in keyboard . parts :
current_path = current_path / directory
keyboard_c_path = current_path / f ' { directory } .c'
if keyboard_c_path . exists ():
files . append ( keyboard_c_path )
return files
def _extract_led_config ( info_data , keyboard ):
"""Scan all <keyboard>.c files for led config
"""
cols = info_data [ 'matrix_size' ][ 'cols' ]
rows = info_data [ 'matrix_size' ][ 'rows' ]
2022-09-21 11:41:18 +10:00
# Determine what feature owns g_led_config
features = info_data . get ( "features" , {})
feature = None
if features . get ( "rgb_matrix" , False ):
feature = "rgb_matrix"
elif features . get ( "led_matrix" , False ):
2022-05-15 22:39:29 +01:00
feature = "led_matrix"
2022-09-21 11:41:18 +10:00
if feature :
# Process
for file in find_keyboard_c ( keyboard ):
try :
ret = find_led_config ( file , cols , rows )
if ret :
info_data [ feature ] = info_data . get ( feature , {})
info_data [ feature ][ "layout" ] = ret
except Exception as e :
_log_warning ( info_data , f 'led_config: { file . name } : { e } ' )
2022-05-15 22:39:29 +01:00
return info_data
2021-07-11 09:43:58 -07:00
def _matrix_size ( info_data ):
"""Add info_data['matrix_size'] if it doesn't exist.
"""
if 'matrix_size' not in info_data and 'matrix_pins' in info_data :
info_data [ 'matrix_size' ] = {}
if 'direct' in info_data [ 'matrix_pins' ]:
info_data [ 'matrix_size' ][ 'cols' ] = len ( info_data [ 'matrix_pins' ][ 'direct' ][ 0 ])
info_data [ 'matrix_size' ][ 'rows' ] = len ( info_data [ 'matrix_pins' ][ 'direct' ])
elif 'cols' in info_data [ 'matrix_pins' ] and 'rows' in info_data [ 'matrix_pins' ]:
info_data [ 'matrix_size' ][ 'cols' ] = len ( info_data [ 'matrix_pins' ][ 'cols' ])
info_data [ 'matrix_size' ][ 'rows' ] = len ( info_data [ 'matrix_pins' ][ 'rows' ])
2022-02-27 12:39:24 +00:00
# Assumption of split common
if 'split' in info_data :
if info_data [ 'split' ] . get ( 'enabled' , False ):
info_data [ 'matrix_size' ][ 'rows' ] *= 2
2021-07-11 09:43:58 -07:00
return info_data
def _check_matrix ( info_data ):
"""Check the matrix to ensure that row/column count is consistent.
"""
if 'matrix_pins' in info_data and 'matrix_size' in info_data :
actual_col_count = info_data [ 'matrix_size' ] . get ( 'cols' , 0 )
actual_row_count = info_data [ 'matrix_size' ] . get ( 'rows' , 0 )
col_count = row_count = 0
if 'direct' in info_data [ 'matrix_pins' ]:
col_count = len ( info_data [ 'matrix_pins' ][ 'direct' ][ 0 ])
row_count = len ( info_data [ 'matrix_pins' ][ 'direct' ])
elif 'cols' in info_data [ 'matrix_pins' ] and 'rows' in info_data [ 'matrix_pins' ]:
col_count = len ( info_data [ 'matrix_pins' ][ 'cols' ])
row_count = len ( info_data [ 'matrix_pins' ][ 'rows' ])
if col_count != actual_col_count and col_count != ( actual_col_count / 2 ):
# FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check.
_log_error ( info_data , f 'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: { col_count } != { actual_col_count } ' )
if row_count != actual_row_count and row_count != ( actual_row_count / 2 ):
# FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check.
_log_error ( info_data , f 'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: { row_count } != { actual_row_count } ' )
2021-08-15 13:30:58 -07:00
def _search_keyboard_h ( keyboard ):
keyboard = Path ( keyboard )
2020-05-26 13:05:41 -07:00
current_path = Path ( 'keyboards/' )
2021-02-27 12:00:50 -08:00
aliases = {}
2020-10-07 10:57:40 +11:00
layouts = {}
2021-02-27 12:00:50 -08:00
2021-08-15 13:30:58 -07:00
for directory in keyboard . parts :
2020-05-26 13:05:41 -07:00
current_path = current_path / directory
keyboard_h = ' %s .h' % ( directory ,)
keyboard_h_path = current_path / keyboard_h
if keyboard_h_path . exists ():
2021-02-27 12:00:50 -08:00
new_layouts , new_aliases = find_layouts ( keyboard_h_path )
layouts . update ( new_layouts )
2020-05-26 13:05:41 -07:00
2021-02-27 12:00:50 -08:00
for alias , alias_text in new_aliases . items ():
if alias_text in layouts :
aliases [ alias ] = alias_text
return layouts , aliases
2020-10-07 10:57:40 +11:00
2020-11-07 09:56:08 -08:00
def _log_error ( info_data , message ):
"""Send an error message to both JSON and the log.
"""
info_data [ 'parse_errors' ] . append ( message )
cli . log . error ( ' %s : %s ' , info_data . get ( 'keyboard_folder' , 'Unknown Keyboard!' ), message )
def _log_warning ( info_data , message ):
"""Send a warning message to both JSON and the log.
"""
info_data [ 'parse_warnings' ] . append ( message )
cli . log . warning ( ' %s : %s ' , info_data . get ( 'keyboard_folder' , 'Unknown Keyboard!' ), message )
2020-05-26 13:05:41 -07:00
def arm_processor_rules ( info_data , rules ):
"""Setup the default info for an ARM board.
"""
info_data [ 'processor_type' ] = 'arm'
info_data [ 'protocol' ] = 'ChibiOS'
2023-01-19 00:27:00 +00:00
info_data [ 'platform_key' ] = 'chibios'
2020-05-26 13:05:41 -07:00
if 'STM32' in info_data [ 'processor' ]:
info_data [ 'platform' ] = 'STM32'
elif 'MCU_SERIES' in rules :
info_data [ 'platform' ] = rules [ 'MCU_SERIES' ]
elif 'ARM_ATSAM' in rules :
info_data [ 'platform' ] = 'ARM_ATSAM'
2023-01-19 00:27:00 +00:00
info_data [ 'platform_key' ] = 'arm_atsam'
2020-05-26 13:05:41 -07:00
return info_data
def avr_processor_rules ( info_data , rules ):
"""Setup the default info for an AVR board.
"""
info_data [ 'processor_type' ] = 'avr'
info_data [ 'platform' ] = rules [ 'ARCH' ] if 'ARCH' in rules else 'unknown'
2023-01-19 00:27:00 +00:00
info_data [ 'platform_key' ] = 'avr'
2022-11-26 04:31:58 +11:00
info_data [ 'protocol' ] = 'V-USB' if info_data [ 'processor' ] in VUSB_PROCESSORS else 'LUFA'
2020-05-26 13:05:41 -07:00
# FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
# info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
return info_data
def unknown_processor_rules ( info_data , rules ):
"""Setup the default keyboard info for unknown boards.
"""
info_data [ 'bootloader' ] = 'unknown'
info_data [ 'platform' ] = 'unknown'
info_data [ 'processor' ] = 'unknown'
info_data [ 'processor_type' ] = 'unknown'
info_data [ 'protocol' ] = 'unknown'
return info_data
def merge_info_jsons ( keyboard , info_data ):
"""Return a merged copy of all the info.json files for a keyboard.
"""
for info_file in find_info_json ( keyboard ):
# Load and validate the JSON data
2021-03-24 09:26:38 -07:00
new_info_data = json_load ( info_file )
2020-05-26 13:05:41 -07:00
if not isinstance ( new_info_data , dict ):
2020-12-19 10:46:30 -08:00
_log_error ( info_data , "Invalid file %s , root object should be a dictionary." % ( str ( info_file ),))
2020-05-26 13:05:41 -07:00
continue
2021-01-09 13:34:14 -08:00
try :
2021-06-24 20:48:53 -07:00
validate ( new_info_data , 'qmk.keyboard.v1' )
2021-01-09 13:34:14 -08:00
except jsonschema . ValidationError as e :
json_path = '.' . join ([ str ( p ) for p in e . absolute_path ])
cli . log . error ( 'Not including data from file: %s ' , info_file )
cli . log . error ( ' \t %s : %s ' , json_path , e . message )
continue
2020-05-26 13:05:41 -07:00
2021-02-27 12:00:50 -08:00
# Merge layout data in
2021-03-22 18:58:07 -07:00
if 'layout_aliases' in new_info_data :
info_data [ 'layout_aliases' ] = { ** info_data . get ( 'layout_aliases' , {}), ** new_info_data [ 'layout_aliases' ]}
del new_info_data [ 'layout_aliases' ]
2021-02-27 12:00:50 -08:00
for layout_name , layout in new_info_data . get ( 'layouts' , {}) . items ():
2021-03-22 18:58:07 -07:00
if layout_name in info_data . get ( 'layout_aliases' , {}):
_log_warning ( info_data , f "info.json uses alias name { layout_name } instead of { info_data [ 'layout_aliases' ][ layout_name ] } " )
layout_name = info_data [ 'layout_aliases' ][ layout_name ]
2021-02-27 12:00:50 -08:00
if layout_name in info_data [ 'layouts' ]:
2021-08-20 16:16:59 +01:00
if len ( info_data [ 'layouts' ][ layout_name ][ 'layout' ]) != len ( layout [ 'layout' ]):
2021-10-17 03:44:56 +11:00
msg = 'Number of keys for %s does not match! info.json specifies %d keys, C macro specifies %d '
_log_error ( info_data , msg % ( layout_name , len ( layout [ 'layout' ]), len ( info_data [ 'layouts' ][ layout_name ][ 'layout' ])))
2021-08-20 16:16:59 +01:00
else :
2023-01-19 00:27:11 +00:00
info_data [ 'layouts' ][ layout_name ][ 'json_layout' ] = True
2021-08-20 16:16:59 +01:00
for new_key , existing_key in zip ( layout [ 'layout' ], info_data [ 'layouts' ][ layout_name ][ 'layout' ]):
existing_key . update ( new_key )
2021-02-27 12:00:50 -08:00
else :
2022-06-10 11:44:48 +01:00
if not all ( 'matrix' in key_data . keys () for key_data in layout [ 'layout' ]):
_log_error ( info_data , f 'Layout " { layout_name } " has no "matrix" definition in either "info.json" or "<keyboard>.h"!' )
else :
layout [ 'c_macro' ] = False
2023-01-19 00:27:11 +00:00
layout [ 'json_layout' ] = True
2022-06-10 11:44:48 +01:00
info_data [ 'layouts' ][ layout_name ] = layout
2020-12-30 10:27:37 -08:00
2021-01-09 13:34:14 -08:00
# Update info_data with the new data
2021-02-27 12:00:50 -08:00
if 'layouts' in new_info_data :
2021-03-22 18:58:07 -07:00
del new_info_data [ 'layouts' ]
2021-02-27 12:00:50 -08:00
2021-01-09 13:34:14 -08:00
deep_update ( info_data , new_info_data )
2020-05-26 13:05:41 -07:00
return info_data
def find_info_json ( keyboard ):
"""Finds all the info.json files associated with a keyboard.
"""
# Find the most specific first
base_path = Path ( 'keyboards' )
keyboard_path = base_path / keyboard
keyboard_parent = keyboard_path . parent
info_jsons = [ keyboard_path / 'info.json' ]
# Add DEFAULT_FOLDER before parents, if present
rules = rules_mk ( keyboard )
if 'DEFAULT_FOLDER' in rules :
info_jsons . append ( Path ( rules [ 'DEFAULT_FOLDER' ]) / 'info.json' )
# Add in parent folders for least specific
for _ in range ( 5 ):
2022-03-26 19:15:25 +00:00
if keyboard_parent == base_path :
2020-05-26 13:05:41 -07:00
break
2022-03-26 19:15:25 +00:00
info_jsons . append ( keyboard_parent / 'info.json' )
2020-05-26 13:05:41 -07:00
keyboard_parent = keyboard_parent . parent
# Return a list of the info.json files that actually exist
return [ info_json for info_json in info_jsons if info_json . exists ()]
2022-03-21 10:57:41 +00:00
def keymap_json_config ( keyboard , keymap ):
"""Extract keymap level config
"""
2023-01-11 01:38:35 +00:00
# TODO: resolve keymap.py and info.py circular dependencies
from qmk.keymap import locate_keymap
2022-03-21 10:57:41 +00:00
keymap_folder = locate_keymap ( keyboard , keymap ) . parent
km_info_json = parse_configurator_json ( keymap_folder / 'keymap.json' )
return km_info_json . get ( 'config' , {})
def keymap_json ( keyboard , keymap ):
"""Generate the info.json data for a specific keymap.
"""
2023-01-11 01:38:35 +00:00
# TODO: resolve keymap.py and info.py circular dependencies
from qmk.keymap import locate_keymap
2022-03-21 10:57:41 +00:00
keymap_folder = locate_keymap ( keyboard , keymap ) . parent
# Files to scan
keymap_config = keymap_folder / 'config.h'
keymap_rules = keymap_folder / 'rules.mk'
keymap_file = keymap_folder / 'keymap.json'
# Build the info.json file
kb_info_json = info_json ( keyboard )
# Merge in the data from keymap.json
km_info_json = keymap_json_config ( keyboard , keymap ) if keymap_file . exists () else {}
deep_update ( kb_info_json , km_info_json )
# Merge in the data from config.h, and rules.mk
_extract_rules_mk ( kb_info_json , parse_rules_mk_file ( keymap_rules ))
_extract_config_h ( kb_info_json , parse_config_h_file ( keymap_config ))
return kb_info_json