#! /usr/bin/env python3 # ----------------------------------------------------------------------------- # Copyright (c) 2012 Ben Blazak # Released under The MIT License (MIT) (see "license.md") # Project located at # ----------------------------------------------------------------------------- """ Generate UI info file (in JSON) Depends on: - the project source code - the project '.map' file (generated by the compiler) """ _FORMAT_DESCRIPTION = (""" /* ---------------------------------------------------------------------------- * Version 0 * ---------------------------------------------------------------------------- * Hopefully the add-hoc conventions are clear enough... I didn't feel like * investing the time in making it a real JSON Schema when there aren't many * validators, and the most current completed draft at the moment (draft 3) is * expired... * ---------------------------------------------------------------------------- * Please note that in general, fields may be added without changing the * version number, and that programs using this format are not required to fill * (or read) any of the given fields. * ------------------------------------------------------------------------- */ var ui_info = { ".meta-data": { // for the JSON file "version": "", "date-generated": "", // format: RFC 3339 "description": "", }, "keyboard-functions": { "<(function name)>": { "position": "", // as given by the .map file "length": "", // as given by the .map file "comments": { "name": "", // more user friendly name "description": "", "notes": [ "", "..." ], "..." } }, "..." }, "layout-matrices": { "<(matrix name)>": { "position": "", // as given by the .map file "length": "" // as given by the .map file }, "..." }, "mappings": { /* * The mappings prefixed with 'matrix' have their elements in the same * order as the .hex file (whatever order that is). The mappings * prefixed with 'physical' will have their elements in an order * corresponding to thier physical position on the keyboard. You can * convert between the two using the relative positions of the key-ids * in 'physical-positions' and 'matrix-positions'. * * The current order of 'physical' mappings is: * -------------------------------------------- * // left hand, spatial positions * 00, 01, 02, 03, 04, 05, 06, * 07, 08, 09, 10, 11, 12, 13, * 14, 15, 16, 17, 18, 19, * 20, 21, 22, 23, 24, 25, 26, * 27, 28, 29, 30, 31, * 32, 33, * 34, 35, 36, * 37, 38, 39, * // right hand, spatial positions * 40, 41, 42, 43, 44, 45, 46, * 47, 48, 49, 50, 51, 52, 53, * 54, 55, 56, 57, 58, 59, * 60, 61, 62, 63, 64, 65, 66, * 67, 68, 69, 70, 71, * 72, 73, * 74, 75, 76, * 77, 78, 79, * -------------------------------------------- */ "physical-positions": [ // list of key-ids "", "..." ], "matrix-positions": [ // list of key-ids "", "..." ], "matrix-layout": [ [ // begin layer [ // begin key "", // keycode "", // press function name (ex: 'kbfun_...') "" // release function name (ex: 'NULL') ], "..." // more keys ], "..." // more layers ] }, "miscellaneous": { "git-commit-date": "", // format: RFC 3339 "git-commit-id": "", "number-of-layers": "" } } """)[1:-1] # ----------------------------------------------------------------------------- import argparse import json import os import re import subprocess import sys # ----------------------------------------------------------------------------- def gen_static(current_date=None, git_commit_date=None, git_commit_id=None): """Generate static information""" return { '.meta-data': { 'version': 0, # the format version number 'date-generated': current_date, 'description': _FORMAT_DESCRIPTION, }, 'miscellaneous': { 'git-commit-date': git_commit_date, # should be passed by makefile 'git-commit-id': git_commit_id, # should be passed by makefile }, } def gen_derived(data): """ Generate derived information Should be called last """ return { 'miscellaneous': { 'number-of-layers': int( data['layout-matrices']['_kb_layout']['length']/(6*14) ), # because 6*14 is the number of bytes/layer for '_kb_layout' # (which is a uint8_t matrix) }, } # ----------------------------------------------------------------------------- def parse_mapfile(map_file_path): """Parse the '.map' file""" def parse_keyboard_function(f, line): """Parse keyboard-functions in the '.map' file""" search = re.search(r'(0x\S+)\s+(0x\S+)', next(f)) position = int( search.group(1), 16 ) length = int( search.group(2), 16 ) search = re.search(r'0x\S+\s+(\S+)', next(f)) name = search.group(1) return { 'keyboard-functions': { name: { 'position': position, 'length': length, }, }, } def parse_layout_matrices(f, line): """Parse layout matrix information in the '.map' file""" name = re.search(r'.progmem.data.(_kb_layout\S*)', line).group(1) search = re.search(r'(0x\S+)\s+(0x\S+)', next(f)) position = int( search.group(1), 16 ) length = int( search.group(2), 16 ) return { 'layout-matrices': { name: { 'position': position, 'length': length, }, }, } # --- parse_mapfile() --- # normalize paths map_file_path = os.path.abspath(map_file_path) # check paths if not os.path.exists(map_file_path): raise ValueError("invalid 'map_file_path' given") output = {} f = open(map_file_path) for line in f: if re.search(r'^\s*\.text\.kbfun_', line): dict_merge(output, parse_keyboard_function(f, line)) elif re.search(r'^\s*\.progmem\.data.*layout', line): dict_merge(output, parse_layout_matrices(f, line)) return output def find_keyboard_functions(source_code_path): """Parse all files in the source directory""" def read_comments(f, line): """ Read in properly formatted multi-line comments - Comments must start with '/*' and end with '*/', each on their own line """ comments = '' while(line.strip() != r'*/'): comments += line[2:].strip()+'\n' line = next(f) return comments def parse_comments(comments): """ Parse an INI style comment string - Fields begin with '[field-name]', and continue until the next field, or the end of the comment - Fields '[name]', '[description]', and '[note]' are treated specially """ def add_field(output, field, value): """Put a field+value pair in 'output', the way we want it, if the pair is valid""" value = value.strip() if field is not None: if field in ('name', 'description'): if field not in output: output[field] = value else: if field == 'note': field = 'notes' if field not in output: output[field] = [] output[field] += [value] # --- parse_comments() --- output = {} field = None value = None for line in comments.split('\n'): line = line.strip() if re.search(r'^\[.*\]$', line): add_field(output, field, value) field = line[1:-1] value = None else: if value is None: value = '' if len(value) > 0 and value[-1] == '.': line = ' '+line value += ' '+line add_field(output, field, value) return output def parse_keyboard_function(f, line, comments): """Parse keyboard-functions in the source code""" search = re.search(r'void\s+(kbfun_\S+)\s*\(void\)', line) name = search.group(1) return { 'keyboard-functions': { name: { 'comments': parse_comments(comments), }, }, } # --- find_keyboard_functions() --- # normalize paths source_code_path = os.path.abspath(source_code_path) # check paths if not os.path.exists(source_code_path): raise ValueError("invalid 'source_code_path' given") output = {} for tup in os.walk(source_code_path): for file_name in tup[2]: # normalize paths file_name = os.path.abspath( os.path.join( tup[0], file_name ) ) # ignore non '.c' files if file_name[-2:] != '.c': continue f = open(file_name) comments = '' for line in f: if line.strip() == r'/*': comments = read_comments(f, line) elif re.search(r'void\s+kbfun_\S+\s*\(void\)', line): dict_merge( output, parse_keyboard_function(f, line, comments) ) return output def gen_mappings(matrix_file_path, layout_file_path): # normalize paths matrix_file_path = os.path.abspath(matrix_file_path) layout_file_path = os.path.abspath(layout_file_path) def parse_matrix_file(matrix_file_path): match = re.search( # find the whole 'KB_MATRIX_LAYER' macro r'#define\s+KB_MATRIX_LAYER\s*\(([^)]+)\)[^{]*\{\{([^#]+)\}\}', open(matrix_file_path).read() ) return { "mappings": { "physical-positions": re.findall(r'k..', match.group(1)), "matrix-positions": re.findall(r'k..|na', match.group(2)), }, } def parse_layout_file(layout_file_path): match = re.findall( # find each whole '_kb_layout*' matrix definition r'(_kb_layout\w*)[^=]*=((?:[^{}]*\{){3}[^=]*(?:[^{}]*\}){3})', subprocess.getoutput("gcc -E '"+layout_file_path+"'") ) layout = {} # collect all the values for (name, matrix) in match: layout[name] = [ re.findall( # find all numbers and function pointers r'[x0-9A-F]+|&\w+|NULL', re.sub( # replace '((void *) 0)' with 'NULL' r'\(\s*\(\s*void\s*\*\s*\)\s*0\s*\)', 'NULL', el ) ) for el in re.findall( # find each whole layer r'(?:[^{}]*\{){2}((?:[^}]|\}\s*,)+)(?:[^{}]*\}){2}', matrix ) ] # make the numbers into actual numbers layout['_kb_layout'] = \ [[eval(el) for el in layer] for layer in layout['_kb_layout']] # remove the preceeding '&' from function pointers for matrix in ('_kb_layout_press', '_kb_layout_release'): layout[matrix] = \ [ [re.sub(r'&', '', el) for el in layer] for layer in layout[matrix] ] return { "mappings": { "matrix-layout": # group them all properly [ [[c, p, r] for (c, p, r) in zip(code, press, release)] for (code, press, release) in zip( layout['_kb_layout'], layout['_kb_layout_press'], layout['_kb_layout_release'] ) ] }, } return dict_merge( parse_matrix_file(matrix_file_path), parse_layout_file(layout_file_path) ) # ----------------------------------------------------------------------------- def dict_merge(a, b): """ Recursively merge two dictionaries - I was looking around for an easy way to do this, and found something [here] (http://www.xormedia.com/recursively-merge-dictionaries-in-python.html). This is pretty close, but i didn't copy it exactly. """ if not isinstance(a, dict) or not isinstance(b, dict): return b for (key, value) in b.items(): if key in a: a[key] = dict_merge(a[key], value) else: a[key] = value return a # ----------------------------------------------------------------------------- def main(): arg_parser = argparse.ArgumentParser( description = 'Generate project data for use with the UI' ) arg_parser.add_argument( '--current-date', help = ( "should be in the format rfc-3339 " + "(e.g. 2006-08-07 12:34:56-06:00)" ), required = True ) arg_parser.add_argument( '--git-commit-date', help = ( "should be in the format rfc-3339 " + "(e.g. 2006-08-07 12:34:56-06:00)" ), required = True ) arg_parser.add_argument( '--git-commit-id', help = "the git commit ID", required = True ) arg_parser.add_argument( '--map-file-path', help = "the path to the '.map' file", required = True ) arg_parser.add_argument( '--source-code-path', help = "the path to the source code directory", required = True ) arg_parser.add_argument( '--matrix-file-path', help = "the path to the matrix file we're using", required = True ) arg_parser.add_argument( '--layout-file-path', help = "the path to the layout file we're using", required = True ) args = arg_parser.parse_args(sys.argv[1:]) output = {} dict_merge( output, gen_static( args.current_date, args.git_commit_date, args.git_commit_id ) ) dict_merge(output, parse_mapfile(args.map_file_path)) dict_merge(output, find_keyboard_functions(args.source_code_path)) dict_merge(output, gen_mappings( args.matrix_file_path, args.layout_file_path )) dict_merge(output, gen_derived(output)) print(json.dumps(output, sort_keys=True, indent=4)) # ----------------------------------------------------------------------------- if __name__ == '__main__': main()