text: #!/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 confgen
  import json
  import os
  import sys
  import tempfile
  from confgen import FatalError, __version__
  
  try:
      from . import kconfiglib
  except Exception:
      sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
      import kconfiglib
  
  # 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('--sdkconfig-rename',
                          help='File with deprecated Kconfig options',
                          required=False)
  
      parser.add_argument('--env', action='append', default=[],
                          help='Environment to set when evaluating the config file', metavar='NAME=VAL')
  
      parser.add_argument('--env-file', type=argparse.FileType('r'),
                          help='Optional file to load environment variables from. Contents '
                               'should be a JSON object where each key/value pair is a variable.')
  
      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
  
      if args.env_file is not None:
          env = json.load(args.env_file)
          os.environ.update(env)
  
      run_server(args.kconfig, args.config, args.sdkconfig_rename)
  
  
  def run_server(kconfig, sdkconfig, sdkconfig_rename, default_version=MAX_PROTOCOL_VERSION):
      config = kconfiglib.Kconfig(kconfig)
      sdkconfig_renames = [sdkconfig_rename] if sdkconfig_rename else []
      sdkconfig_renames += os.environ.get("COMPONENT_SDKCONFIG_RENAMES", "").split()
      deprecated_options = confgen.DeprecatedOptions(config.config_prefix, path_rename_files=sdkconfig_renames)
      f_o = tempfile.NamedTemporaryFile(mode='w+b', delete=False)
      try:
          with open(sdkconfig, mode='rb') as f_i:
              f_o.write(f_i.read())
          f_o.close()  # need to close as DeprecatedOptions will reopen, and Windows only allows one open file
          deprecated_options.replace(sdkconfig_in=f_o.name, sdkconfig_out=sdkconfig)
      finally:
          os.unlink(f_o.name)
      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")
      sys.stdout.flush()
  
      while True:
          line = sys.stdin.readline()
          if not line:
              break
          try:
              req = json.loads(line)
          except ValueError as e:  # json module throws JSONDecodeError (sublcass of ValueError) on Py3 but ValueError on Py2
              response = {"version": default_version, "error": ["JSON formatting error: %s" % e]}
              json.dump(response, sys.stdout)
              print("\n")
              sys.stdout.flush()
              continue
          before = confgen.get_json_values(config)
          before_ranges = get_ranges(config)
          before_visible = get_visible(config)
  
          if "load" in req:  # load a new sdkconfig
  
              if req.get("version", default_version) == 1:
                  # for V1 protocol, send all items when loading new sdkconfig.
                  # (V2+ will only send changes, same as when setting an item)
                  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(deprecated_options, 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")
          sys.stdout.flush()
  
  
  def handle_request(deprecated_options, 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(deprecated_options, 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)
              elif sym.type == kconfiglib.HEX:
                  try:
                      if not isinstance(val, int):
                          val = int(val, 16)  # input can be a decimal JSON value or a string of hex digits
                      sym.set_value(hex(val))
                  except ValueError:
                      error.append("Hex symbol %s can accept a decimal integer or a string of hex digits, only")
              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)
  