OVMS3-idf/tools/kconfig_new/confserver.py
Angus Gratton 90ddbaac16 confserver: Add support for new V2 protocol
V2 adds:
* Independent result for visibility (showing/hiding menus)
* Includes adding IDs for all items (menus & symbols) in kconfig_menus.json

Still backwards compatible with V1, with some small changes (menu items now listed in results).

Also added some protocol docs, changed the "listening on stdin" message to come after any kconfiglib warnings
2019-03-15 17:37:09 +11:00

253 lines
8.7 KiB
Python
Executable file

#!/usr/bin/env python
#
# Long-running server process uses stdin & stdout to communicate JSON
# with a caller
#
from __future__ import print_function
import argparse
import json
import kconfiglib
import os
import sys
import confgen
from confgen import FatalError, __version__
# Min/Max supported protocol versions
MIN_PROTOCOL_VERSION = 1
MAX_PROTOCOL_VERSION = 2
def main():
parser = argparse.ArgumentParser(description='confserver.py v%s - Config Generation Tool' % __version__, prog=os.path.basename(sys.argv[0]))
parser.add_argument('--config',
help='Project configuration settings',
required=True)
parser.add_argument('--kconfig',
help='KConfig file with config item definitions',
required=True)
parser.add_argument('--env', action='append', default=[],
help='Environment to set when evaluating the config file', metavar='NAME=VAL')
parser.add_argument('--version', help='Set protocol version to use on initial status',
type=int, default=MAX_PROTOCOL_VERSION)
args = parser.parse_args()
if args.version < MIN_PROTOCOL_VERSION:
print("Version %d is older than minimum supported protocol version %d. Client is much older than ESP-IDF version?" %
(args.version, MIN_PROTOCOL_VERSION))
if args.version > MAX_PROTOCOL_VERSION:
print("Version %d is newer than maximum supported protocol version %d. Client is newer than ESP-IDF version?" %
(args.version, MAX_PROTOCOL_VERSION))
try:
args.env = [(name,value) for (name,value) in (e.split("=",1) for e in args.env)]
except ValueError:
print("--env arguments must each contain =. To unset an environment variable, use 'ENV='")
sys.exit(1)
for name, value in args.env:
os.environ[name] = value
run_server(args.kconfig, args.config)
def run_server(kconfig, sdkconfig, default_version=MAX_PROTOCOL_VERSION):
config = kconfiglib.Kconfig(kconfig)
config.load_config(sdkconfig)
print("Server running, waiting for requests on stdin...", file=sys.stderr)
config_dict = confgen.get_json_values(config)
ranges_dict = get_ranges(config)
visible_dict = get_visible(config)
if default_version == 1:
# V1: no 'visibility' key, send value None for any invisible item
values_dict = dict((k, v if visible_dict[k] else False) for (k,v) in config_dict.items())
json.dump({"version": 1, "values": values_dict, "ranges": ranges_dict}, sys.stdout)
else:
# V2 onwards: separate visibility from version
json.dump({"version": default_version, "values": config_dict, "ranges": ranges_dict, "visible": visible_dict}, sys.stdout)
print("\n")
while True:
line = sys.stdin.readline()
if not line:
break
req = json.loads(line)
before = confgen.get_json_values(config)
before_ranges = get_ranges(config)
before_visible = get_visible(config)
if "load" in req: # if we're loading a different sdkconfig, response should have all items in it
before = {}
before_ranges = {}
before_visible = {}
# if no new filename is supplied, use existing sdkconfig path, otherwise update the path
if req["load"] is None:
req["load"] = sdkconfig
else:
sdkconfig = req["load"]
if "save" in req:
if req["save"] is None:
req["save"] = sdkconfig
else:
sdkconfig = req["save"]
error = handle_request(config, req)
after = confgen.get_json_values(config)
after_ranges = get_ranges(config)
after_visible = get_visible(config)
values_diff = diff(before, after)
ranges_diff = diff(before_ranges, after_ranges)
visible_diff = diff(before_visible, after_visible)
if req["version"] == 1:
# V1 response, invisible items have value None
for k in (k for (k,v) in visible_diff.items() if not v):
values_diff[k] = None
response = {"version": 1, "values": values_diff, "ranges": ranges_diff}
else:
# V2+ response, separate visibility values
response = {"version": req["version"], "values": values_diff, "ranges": ranges_diff, "visible": visible_diff}
if error:
for e in error:
print("Error: %s" % e, file=sys.stderr)
response["error"] = error
json.dump(response, sys.stdout)
print("\n")
def handle_request(config, req):
if "version" not in req:
return ["All requests must have a 'version'"]
if req["version"] < MIN_PROTOCOL_VERSION or req["version"] > MAX_PROTOCOL_VERSION:
return ["Unsupported request version %d. Server supports versions %d-%d" % (
req["version"],
MIN_PROTOCOL_VERSION,
MAX_PROTOCOL_VERSION)]
error = []
if "load" in req:
print("Loading config from %s..." % req["load"], file=sys.stderr)
try:
config.load_config(req["load"])
except Exception as e:
error += ["Failed to load from %s: %s" % (req["load"], e)]
if "set" in req:
handle_set(config, error, req["set"])
if "save" in req:
try:
print("Saving config to %s..." % req["save"], file=sys.stderr)
confgen.write_config(config, req["save"])
except Exception as e:
error += ["Failed to save to %s: %s" % (req["save"], e)]
return error
def handle_set(config, error, to_set):
missing = [k for k in to_set if k not in config.syms]
if missing:
error.append("The following config symbol(s) were not found: %s" % (", ".join(missing)))
# replace name keys with the full config symbol for each key:
to_set = dict((config.syms[k],v) for (k,v) in to_set.items() if k not in missing)
# Work through the list of values to set, noting that
# some may not be immediately applicable (maybe they depend
# on another value which is being set). Therefore, defer
# knowing if any value is unsettable until then end
while len(to_set):
set_pass = [(k,v) for (k,v) in to_set.items() if k.visibility]
if not set_pass:
break # no visible keys left
for (sym,val) in set_pass:
if sym.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
if val is True:
sym.set_value(2)
elif val is False:
sym.set_value(0)
else:
error.append("Boolean symbol %s only accepts true/false values" % sym.name)
else:
sym.set_value(str(val))
print("Set %s" % sym.name)
del to_set[sym]
if len(to_set):
error.append("The following config symbol(s) were not visible so were not updated: %s" % (", ".join(s.name for s in to_set)))
def diff(before, after):
"""
Return a dictionary with the difference between 'before' and 'after',
for items which are present in 'after' dictionary
"""
diff = dict((k,v) for (k,v) in after.items() if before.get(k, None) != v)
return diff
def get_ranges(config):
ranges_dict = {}
def handle_node(node):
sym = node.item
if not isinstance(sym, kconfiglib.Symbol):
return
active_range = sym.active_range
if active_range[0] is not None:
ranges_dict[sym.name] = active_range
config.walk_menu(handle_node)
return ranges_dict
def get_visible(config):
"""
Return a dict mapping node IDs (config names or menu node IDs) to True/False for their visibility
"""
result = {}
menus = []
# when walking the menu the first time, only
# record whether the config symbols are visible
# and make a list of menu nodes (that are not symbols)
def handle_node(node):
sym = node.item
try:
visible = (sym.visibility != 0)
result[node] = visible
except AttributeError:
menus.append(node)
config.walk_menu(handle_node)
# now, figure out visibility for each menu. A menu is visible if any of its children are visible
for m in reversed(menus): # reverse to start at leaf nodes
result[m] = any(v for (n,v) in result.items() if n.parent == m)
# return a dict mapping the node ID to its visibility.
result = dict((confgen.get_menu_node_id(n),v) for (n,v) in result.items())
return result
if __name__ == '__main__':
try:
main()
except FatalError as e:
print("A fatal error occurred: %s" % e, file=sys.stderr)
sys.exit(2)