2012-08-23 10:31:25 +02:00
|
|
|
#! /usr/bin/env python3
|
|
|
|
# -----------------------------------------------------------------------------
|
2012-10-14 07:54:12 +02:00
|
|
|
# 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>
|
|
|
|
# -----------------------------------------------------------------------------
|
2012-08-23 10:31:25 +02:00
|
|
|
|
|
|
|
"""
|
2012-10-14 07:54:12 +02:00
|
|
|
Generate UI info file (in JSON)
|
2012-08-23 10:31:25 +02:00
|
|
|
|
|
|
|
Depends on:
|
|
|
|
- the project source code
|
|
|
|
- the project '.map' file (generated by the compiler)
|
|
|
|
"""
|
|
|
|
|
2012-10-14 07:54:12 +02:00
|
|
|
_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
|
|
|
|
},
|
|
|
|
"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]
|
|
|
|
|
2012-08-23 10:31:25 +02:00
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
2012-10-07 09:18:59 +02:00
|
|
|
def gen_static(current_date=None, git_commit_date=None, git_commit_id=None):
|
2012-08-23 10:31:25 +02:00
|
|
|
"""Generate static information"""
|
|
|
|
|
|
|
|
return {
|
|
|
|
'.meta-data': {
|
|
|
|
'version': 0, # the format version number
|
2012-10-07 09:18:59 +02:00
|
|
|
'date-generated': current_date,
|
2012-10-14 07:54:12 +02:00
|
|
|
'description': _FORMAT_DESCRIPTION,
|
2012-08-23 10:31:25 +02:00
|
|
|
},
|
|
|
|
'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):
|
2012-10-07 09:18:59 +02:00
|
|
|
"""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 )
|
2012-08-23 10:31:25 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
'layout-matrices': {
|
2012-10-07 09:18:59 +02:00
|
|
|
name: {
|
|
|
|
'position': position,
|
|
|
|
'length': length,
|
2012-08-23 10:31:25 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
# --- 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
|
|
|
|
|
|
|
|
|
2012-10-07 09:18:59 +02:00
|
|
|
def find_keyboard_functions(source_code_path):
|
2012-08-23 10:31:25 +02:00
|
|
|
"""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),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2012-10-07 09:18:59 +02:00
|
|
|
# --- find_keyboard_functions() ---
|
2012-08-23 10:31:25 +02:00
|
|
|
|
|
|
|
# normalize paths
|
2012-10-07 09:18:59 +02:00
|
|
|
source_code_path = os.path.abspath(source_code_path)
|
2012-08-23 10:31:25 +02:00
|
|
|
# check paths
|
|
|
|
if not os.path.exists(source_code_path):
|
2012-10-07 09:18:59 +02:00
|
|
|
raise ValueError("invalid 'source_code_path' given")
|
2012-08-23 10:31:25 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2012-10-07 09:18:59 +02:00
|
|
|
|
2012-10-09 03:01:06 +02:00
|
|
|
def gen_mappings(matrix_file_path, layout_file_path):
|
2012-10-07 09:18:59 +02:00
|
|
|
# normalize paths
|
|
|
|
matrix_file_path = os.path.abspath(matrix_file_path)
|
2012-10-09 03:01:06 +02:00
|
|
|
layout_file_path = os.path.abspath(layout_file_path)
|
2012-10-07 09:18:59 +02:00
|
|
|
|
2012-10-09 03:01:06 +02:00
|
|
|
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']]
|
2012-10-14 07:54:12 +02:00
|
|
|
# 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] ]
|
2012-10-09 03:01:06 +02:00
|
|
|
|
|
|
|
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) )
|
2012-10-07 09:18:59 +02:00
|
|
|
|
|
|
|
|
2012-08-23 10:31:25 +02:00
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
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' )
|
|
|
|
|
2012-10-07 09:18:59 +02:00
|
|
|
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 )
|
2012-08-23 10:31:25 +02:00
|
|
|
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 )
|
2012-10-07 09:18:59 +02:00
|
|
|
arg_parser.add_argument(
|
|
|
|
'--matrix-file-path',
|
|
|
|
help = "the path to the matrix file we're using",
|
|
|
|
required = True )
|
2012-10-09 03:01:06 +02:00
|
|
|
arg_parser.add_argument(
|
|
|
|
'--layout-file-path',
|
|
|
|
help = "the path to the layout file we're using",
|
|
|
|
required = True )
|
2012-08-23 10:31:25 +02:00
|
|
|
|
|
|
|
args = arg_parser.parse_args(sys.argv[1:])
|
|
|
|
|
|
|
|
output = {}
|
2012-10-07 09:18:59 +02:00
|
|
|
dict_merge( output, gen_static( args.current_date,
|
|
|
|
args.git_commit_date,
|
|
|
|
args.git_commit_id ) )
|
2012-08-23 10:31:25 +02:00
|
|
|
dict_merge(output, parse_mapfile(args.map_file_path))
|
2012-10-07 09:18:59 +02:00
|
|
|
dict_merge(output, find_keyboard_functions(args.source_code_path))
|
2012-10-09 03:01:06 +02:00
|
|
|
dict_merge(output, gen_mappings( args.matrix_file_path,
|
|
|
|
args.layout_file_path ))
|
2012-08-23 10:31:25 +02:00
|
|
|
dict_merge(output, gen_derived(output))
|
|
|
|
|
|
|
|
print(json.dumps(output, sort_keys=True, indent=4))
|
|
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|
|
|
|
|