ergodox-firmware/build-scripts/gen-ui-info.py

476 lines
13 KiB
Python
Executable File

#! /usr/bin/env python3
# -----------------------------------------------------------------------------
# Copyright (c) 2012 Ben Blazak <benblazak.dev@gmail.com>
# Released under The MIT License (MIT) (see "license.md")
# Project located at <https://github.com/benblazak/ergodox-firmware>
# -----------------------------------------------------------------------------
"""
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": "<number>",
"date-generated": "<string>", // format: RFC 3339
"description": "<string>",
},
"keyboard-functions": {
"<(function name)>": {
"position": "<number>", // as given by the .map file
"length": "<number>", // as given by the .map file
"comments": {
"name": "<string>", // more user friendly name
"description": "<string>",
"notes": [
"<string>",
"..."
],
"..."
}
},
"..."
},
"layout-matrices": {
"<(matrix name)>": {
"position": "<number>", // as given by the .map file
"length": "<number>" // 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
"<string>", "..."
],
"matrix-positions": [ // list of key-ids
"<string>", "..."
],
"matrix-layout": [
[ // begin layer
[ // begin key
"<number>", // keycode
"<string>", // press function name (ex: 'kbfun_...')
"<string>" // release function name (ex: 'NULL')
],
"..." // more keys
],
"..." // more layers
]
},
"miscellaneous": {
"git-commit-date": "<string>", // format: RFC 3339
"git-commit-id": "<string>",
"number-of-layers": "<number>"
}
}
""")[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()