+ for subname, subdesc in iterate_messages(desc, names):
+ for extension in subdesc.extension:
+ yield subname, extension
+
+def toposort2(data):
+ '''Topological sort.
+ From http://code.activestate.com/recipes/577413-topological-sort/
+ This function is under the MIT license.
+ '''
+ for k, v in list(data.items()):
+ v.discard(k) # Ignore self dependencies
+ extra_items_in_deps = reduce(set.union, list(data.values()), set()) - set(data.keys())
+ data.update(dict([(item, set()) for item in extra_items_in_deps]))
+ while True:
+ ordered = set(item for item,dep in list(data.items()) if not dep)
+ if not ordered:
+ break
+ for item in sorted(ordered):
+ yield item
+ data = dict([(item, (dep - ordered)) for item,dep in list(data.items())
+ if item not in ordered])
+ assert not data, "A cyclic dependency exists amongst %r" % data
+
+def sort_dependencies(messages):
+ '''Sort a list of Messages based on dependencies.'''
+ dependencies = {}
+ message_by_name = {}
+ for message in messages:
+ dependencies[str(message.name)] = set(message.get_dependencies())
+ message_by_name[str(message.name)] = message
+
+ for msgname in toposort2(dependencies):
+ if msgname in message_by_name:
+ yield message_by_name[msgname]
+
+def make_identifier(headername):
+ '''Make #ifndef identifier that contains uppercase A-Z and digits 0-9'''
+ result = ""
+ for c in headername.upper():
+ if c.isalnum():
+ result += c
+ else:
+ result += '_'
+ return result
+
+class ProtoFile:
+ def __init__(self, fdesc, file_options):
+ '''Takes a FileDescriptorProto and parses it.'''
+ self.fdesc = fdesc
+ self.file_options = file_options
+ self.dependencies = {}
+ self.parse()
+
+ # Some of types used in this file probably come from the file itself.
+ # Thus it has implicit dependency on itself.
+ self.add_dependency(self)
+
+ def parse(self):
+ self.enums = []
+ self.messages = []
+ self.extensions = []
+
+ if self.fdesc.package:
+ base_name = Names(self.fdesc.package.split('.'))
+ else:
+ base_name = Names()
+
+ for enum in self.fdesc.enum_type:
+ enum_options = get_nanopb_suboptions(enum, self.file_options, base_name + enum.name)
+ self.enums.append(Enum(base_name, enum, enum_options))
+
+ for names, message in iterate_messages(self.fdesc, base_name):
+ message_options = get_nanopb_suboptions(message, self.file_options, names)
+
+ if message_options.skip_message:
+ continue
+
+ self.messages.append(Message(names, message, message_options))
+ for enum in message.enum_type:
+ enum_options = get_nanopb_suboptions(enum, message_options, names + enum.name)
+ self.enums.append(Enum(names, enum, enum_options))
+
+ for names, extension in iterate_extensions(self.fdesc, base_name):
+ field_options = get_nanopb_suboptions(extension, self.file_options, names + extension.name)
+ if field_options.type != nanopb_pb2.FT_IGNORE:
+ self.extensions.append(ExtensionField(names, extension, field_options))
+
+ def add_dependency(self, other):
+ for enum in other.enums:
+ self.dependencies[str(enum.names)] = enum
+
+ for msg in other.messages:
+ self.dependencies[str(msg.name)] = msg
+
+ # Fix field default values where enum short names are used.
+ for enum in other.enums:
+ if not enum.options.long_names:
+ for message in self.messages:
+ for field in message.fields:
+ if field.default in enum.value_longnames:
+ idx = enum.value_longnames.index(field.default)
+ field.default = enum.values[idx][0]
+
+ # Fix field data types where enums have negative values.
+ for enum in other.enums:
+ if not enum.has_negative():
+ for message in self.messages:
+ for field in message.fields:
+ if field.pbtype == 'ENUM' and field.ctype == enum.names:
+ field.pbtype = 'UENUM'
+
+ def generate_header(self, includes, headername, options):
+ '''Generate content for a header file.
+ Generates strings, which should be concatenated and stored to file.
+ '''
+
+ yield '/* Automatically generated nanopb header */\n'
+ if options.notimestamp:
+ yield '/* Generated by %s */\n\n' % (nanopb_version)
+ else:
+ yield '/* Generated by %s at %s. */\n\n' % (nanopb_version, time.asctime())
+
+ if self.fdesc.package:
+ symbol = make_identifier(self.fdesc.package + '_' + headername)
+ else:
+ symbol = make_identifier(headername)
+ yield '#ifndef PB_%s_INCLUDED\n' % symbol
+ yield '#define PB_%s_INCLUDED\n' % symbol
+ try:
+ yield options.libformat % ('pb.h')
+ except TypeError:
+ # no %s specified - use whatever was passed in as options.libformat
+ yield options.libformat
+ yield '\n'
+
+ for incfile in includes:
+ noext = os.path.splitext(incfile)[0]
+ yield options.genformat % (noext + options.extension + '.h')
+ yield '\n'
+
+ yield '/* @@protoc_insertion_point(includes) */\n'
+
+ yield '#if PB_PROTO_HEADER_VERSION != 30\n'
+ yield '#error Regenerate this file with the current version of nanopb generator.\n'
+ yield '#endif\n'
+ yield '\n'
+
+ yield '#ifdef __cplusplus\n'
+ yield 'extern "C" {\n'
+ yield '#endif\n\n'
+
+ if self.enums:
+ yield '/* Enum definitions */\n'
+ for enum in self.enums:
+ yield str(enum) + '\n\n'
+
+ if self.messages:
+ yield '/* Struct definitions */\n'
+ for msg in sort_dependencies(self.messages):
+ yield msg.types()
+ yield str(msg) + '\n\n'
+
+ if self.extensions:
+ yield '/* Extensions */\n'
+ for extension in self.extensions:
+ yield extension.extension_decl()
+ yield '\n'
+
+ if self.messages:
+ yield '/* Default values for struct fields */\n'
+ for msg in self.messages:
+ yield msg.default_decl(True)
+ yield '\n'
+
+ yield '/* Initializer values for message structs */\n'
+ for msg in self.messages:
+ identifier = '%s_init_default' % msg.name
+ yield '#define %-40s %s\n' % (identifier, msg.get_initializer(False))
+ for msg in self.messages:
+ identifier = '%s_init_zero' % msg.name
+ yield '#define %-40s %s\n' % (identifier, msg.get_initializer(True))
+ yield '\n'
+
+ yield '/* Field tags (for use in manual encoding/decoding) */\n'
+ for msg in sort_dependencies(self.messages):
+ for field in msg.fields:
+ yield field.tags()
+ for extension in self.extensions:
+ yield extension.tags()
+ yield '\n'
+
+ yield '/* Struct field encoding specification for nanopb */\n'
+ for msg in self.messages:
+ yield msg.fields_declaration() + '\n'
+ yield '\n'
+
+ yield '/* Maximum encoded size of messages (where known) */\n'
+ for msg in self.messages:
+ msize = msg.encoded_size(self.dependencies)
+ identifier = '%s_size' % msg.name
+ if msize is not None:
+ yield '#define %-40s %s\n' % (identifier, msize)
+ else:
+ yield '/* %s depends on runtime parameters */\n' % identifier
+ yield '\n'
+
+ yield '/* Message IDs (where set with "msgid" option) */\n'
+
+ yield '#ifdef PB_MSGID\n'
+ for msg in self.messages:
+ if hasattr(msg,'msgid'):
+ yield '#define PB_MSG_%d %s\n' % (msg.msgid, msg.name)
+ yield '\n'
+
+ symbol = make_identifier(headername.split('.')[0])
+ yield '#define %s_MESSAGES \\\n' % symbol
+
+ for msg in self.messages:
+ m = "-1"
+ msize = msg.encoded_size(self.dependencies)
+ if msize is not None:
+ m = msize
+ if hasattr(msg,'msgid'):
+ yield '\tPB_MSG(%d,%s,%s) \\\n' % (msg.msgid, m, msg.name)
+ yield '\n'
+
+ for msg in self.messages:
+ if hasattr(msg,'msgid'):
+ yield '#define %s_msgid %d\n' % (msg.name, msg.msgid)
+ yield '\n'
+
+ yield '#endif\n\n'
+
+ yield '#ifdef __cplusplus\n'
+ yield '} /* extern "C" */\n'
+ yield '#endif\n'
+
+ # End of header
+ yield '/* @@protoc_insertion_point(eof) */\n'
+ yield '\n#endif\n'
+
+ def generate_source(self, headername, options):
+ '''Generate content for a source file.'''
+
+ yield '/* Automatically generated nanopb constant definitions */\n'
+ if options.notimestamp:
+ yield '/* Generated by %s */\n\n' % (nanopb_version)
+ else:
+ yield '/* Generated by %s at %s. */\n\n' % (nanopb_version, time.asctime())
+ yield options.genformat % (headername)
+ yield '\n'
+ yield '/* @@protoc_insertion_point(includes) */\n'
+
+ yield '#if PB_PROTO_HEADER_VERSION != 30\n'
+ yield '#error Regenerate this file with the current version of nanopb generator.\n'
+ yield '#endif\n'
+ yield '\n'
+
+ for msg in self.messages:
+ yield msg.default_decl(False)
+
+ yield '\n\n'
+
+ for msg in self.messages:
+ yield msg.fields_definition() + '\n\n'
+
+ for ext in self.extensions:
+ yield ext.extension_def() + '\n'
+
+ for enum in self.enums:
+ yield enum.enum_to_string_definition() + '\n'
+
+ # Add checks for numeric limits
+ if self.messages:
+ largest_msg = max(self.messages, key = lambda m: m.count_required_fields())
+ largest_count = largest_msg.count_required_fields()
+ if largest_count > 64:
+ yield '\n/* Check that missing required fields will be properly detected */\n'
+ yield '#if PB_MAX_REQUIRED_FIELDS < %d\n' % largest_count
+ yield '#error Properly detecting missing required fields in %s requires \\\n' % largest_msg.name
+ yield ' setting PB_MAX_REQUIRED_FIELDS to %d or more.\n' % largest_count
+ yield '#endif\n'
+
+ max_field = FieldMaxSize()
+ checks_msgnames = []
+ for msg in self.messages:
+ checks_msgnames.append(msg.name)
+ for field in msg.fields:
+ max_field.extend(field.largest_field_value())
+
+ worst = max_field.worst
+ worst_field = max_field.worst_field
+ checks = max_field.checks
+
+ if worst > 255 or checks:
+ yield '\n/* Check that field information fits in pb_field_t */\n'
+
+ if worst > 65535 or checks:
+ yield '#if !defined(PB_FIELD_32BIT)\n'
+ if worst > 65535:
+ yield '#error Field descriptor for %s is too large. Define PB_FIELD_32BIT to fix this.\n' % worst_field
+ else:
+ assertion = ' && '.join(str(c) + ' < 65536' for c in checks)
+ msgs = '_'.join(str(n) for n in checks_msgnames)
+ yield '/* If you get an error here, it means that you need to define PB_FIELD_32BIT\n'
+ yield ' * compile-time option. You can do that in pb.h or on compiler command line.\n'
+ yield ' * \n'
+ yield ' * The reason you need to do this is that some of your messages contain tag\n'
+ yield ' * numbers or field sizes that are larger than what can fit in 8 or 16 bit\n'
+ yield ' * field descriptors.\n'
+ yield ' */\n'
+ yield 'PB_STATIC_ASSERT((%s), YOU_MUST_DEFINE_PB_FIELD_32BIT_FOR_MESSAGES_%s)\n'%(assertion,msgs)
+ yield '#endif\n\n'
+
+ if worst < 65536:
+ yield '#if !defined(PB_FIELD_16BIT) && !defined(PB_FIELD_32BIT)\n'
+ if worst > 255:
+ yield '#error Field descriptor for %s is too large. Define PB_FIELD_16BIT to fix this.\n' % worst_field
+ else:
+ assertion = ' && '.join(str(c) + ' < 256' for c in checks)
+ msgs = '_'.join(str(n) for n in checks_msgnames)
+ yield '/* If you get an error here, it means that you need to define PB_FIELD_16BIT\n'
+ yield ' * compile-time option. You can do that in pb.h or on compiler command line.\n'
+ yield ' * \n'
+ yield ' * The reason you need to do this is that some of your messages contain tag\n'
+ yield ' * numbers or field sizes that are larger than what can fit in the default\n'
+ yield ' * 8 bit descriptors.\n'
+ yield ' */\n'
+ yield 'PB_STATIC_ASSERT((%s), YOU_MUST_DEFINE_PB_FIELD_16BIT_FOR_MESSAGES_%s)\n'%(assertion,msgs)
+ yield '#endif\n\n'
+
+ # Add check for sizeof(double)
+ has_double = False
+ for msg in self.messages:
+ for field in msg.fields:
+ if field.ctype == 'double':
+ has_double = True
+
+ if has_double:
+ yield '\n'
+ yield '/* On some platforms (such as AVR), double is really float.\n'
+ yield ' * These are not directly supported by nanopb, but see example_avr_double.\n'
+ yield ' * To get rid of this error, remove any double fields from your .proto.\n'
+ yield ' */\n'
+ yield 'PB_STATIC_ASSERT(sizeof(double) == 8, DOUBLE_MUST_BE_8_BYTES)\n'
+
+ yield '\n'
+ yield '/* @@protoc_insertion_point(eof) */\n'
+
+# ---------------------------------------------------------------------------
+# Options parsing for the .proto files
+# ---------------------------------------------------------------------------
+
+from fnmatch import fnmatch
+
+def read_options_file(infile):
+ '''Parse a separate options file to list:
+ [(namemask, options), ...]
+ '''
+ results = []
+ data = infile.read()
+ data = re.sub('/\*.*?\*/', '', data, flags = re.MULTILINE)
+ data = re.sub('//.*?$', '', data, flags = re.MULTILINE)
+ data = re.sub('#.*?$', '', data, flags = re.MULTILINE)
+ for i, line in enumerate(data.split('\n')):
+ line = line.strip()
+ if not line:
+ continue
+
+ parts = line.split(None, 1)
+
+ if len(parts) < 2:
+ sys.stderr.write("%s:%d: " % (infile.name, i + 1) +
+ "Option lines should have space between field name and options. " +
+ "Skipping line: '%s'\n" % line)
+ continue
+
+ opts = nanopb_pb2.NanoPBOptions()
+
+ try:
+ text_format.Merge(parts[1], opts)
+ except Exception as e:
+ sys.stderr.write("%s:%d: " % (infile.name, i + 1) +
+ "Unparseable option line: '%s'. " % line +
+ "Error: %s\n" % str(e))
+ continue
+ results.append((parts[0], opts))
+
+ return results
+
+class Globals:
+ '''Ugly global variables, should find a good way to pass these.'''
+ verbose_options = False
+ separate_options = []
+ matched_namemasks = set()
+
+def get_nanopb_suboptions(subdesc, options, name):
+ '''Get copy of options, and merge information from subdesc.'''
+ new_options = nanopb_pb2.NanoPBOptions()
+ new_options.CopyFrom(options)
+
+ # Handle options defined in a separate file
+ dotname = '.'.join(name.parts)
+ for namemask, options in Globals.separate_options:
+ if fnmatch(dotname, namemask):
+ Globals.matched_namemasks.add(namemask)
+ new_options.MergeFrom(options)
+
+ if hasattr(subdesc, 'syntax') and subdesc.syntax == "proto3":
+ new_options.proto3 = True
+
+ # Handle options defined in .proto
+ if isinstance(subdesc.options, descriptor.FieldOptions):
+ ext_type = nanopb_pb2.nanopb
+ elif isinstance(subdesc.options, descriptor.FileOptions):
+ ext_type = nanopb_pb2.nanopb_fileopt
+ elif isinstance(subdesc.options, descriptor.MessageOptions):
+ ext_type = nanopb_pb2.nanopb_msgopt
+ elif isinstance(subdesc.options, descriptor.EnumOptions):
+ ext_type = nanopb_pb2.nanopb_enumopt
+ else:
+ raise Exception("Unknown options type")
+
+ if subdesc.options.HasExtension(ext_type):
+ ext = subdesc.options.Extensions[ext_type]
+ new_options.MergeFrom(ext)
+
+ if Globals.verbose_options:
+ sys.stderr.write("Options for " + dotname + ": ")
+ sys.stderr.write(text_format.MessageToString(new_options) + "\n")
+
+ return new_options
+
+
+# ---------------------------------------------------------------------------
+# Command line interface
+# ---------------------------------------------------------------------------
+
+import sys
+import os.path
+from optparse import OptionParser
+
+optparser = OptionParser(
+ usage = "Usage: nanopb_generator.py [options] file.pb ...",
+ epilog = "Compile file.pb from file.proto by: 'protoc -ofile.pb file.proto'. " +
+ "Output will be written to file.pb.h and file.pb.c.")
+optparser.add_option("-x", dest="exclude", metavar="FILE", action="append", default=[],
+ help="Exclude file from generated #include list.")
+optparser.add_option("-e", "--extension", dest="extension", metavar="EXTENSION", default=".pb",
+ help="Set extension to use instead of '.pb' for generated files. [default: %default]")
+optparser.add_option("-f", "--options-file", dest="options_file", metavar="FILE", default="%s.options",
+ help="Set name of a separate generator options file.")
+optparser.add_option("-I", "--options-path", dest="options_path", metavar="DIR",
+ action="append", default = [],
+ help="Search for .options files additionally in this path")
+optparser.add_option("-D", "--output-dir", dest="output_dir",
+ metavar="OUTPUTDIR", default=None,
+ help="Output directory of .pb.h and .pb.c files")
+optparser.add_option("-Q", "--generated-include-format", dest="genformat",
+ metavar="FORMAT", default='#include "%s"\n',
+ help="Set format string to use for including other .pb.h files. [default: %default]")
+optparser.add_option("-L", "--library-include-format", dest="libformat",
+ metavar="FORMAT", default='#include <%s>\n',
+ help="Set format string to use for including the nanopb pb.h header. [default: %default]")
+optparser.add_option("-T", "--no-timestamp", dest="notimestamp", action="store_true", default=False,
+ help="Don't add timestamp to .pb.h and .pb.c preambles")
+optparser.add_option("-q", "--quiet", dest="quiet", action="store_true", default=False,
+ help="Don't print anything except errors.")
+optparser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
+ help="Print more information.")
+optparser.add_option("-s", dest="settings", metavar="OPTION:VALUE", action="append", default=[],
+ help="Set generator option (max_size, max_count etc.).")
+
+def parse_file(filename, fdesc, options):
+ '''Parse a single file. Returns a ProtoFile instance.'''
+ toplevel_options = nanopb_pb2.NanoPBOptions()
+ for s in options.settings:
+ text_format.Merge(s, toplevel_options)
+
+ if not fdesc:
+ data = open(filename, 'rb').read()
+ fdesc = descriptor.FileDescriptorSet.FromString(data).file[0]
+
+ # Check if there is a separate .options file
+ had_abspath = False
+ try:
+ optfilename = options.options_file % os.path.splitext(filename)[0]
+ except TypeError:
+ # No %s specified, use the filename as-is
+ optfilename = options.options_file
+ had_abspath = True
+
+ paths = ['.'] + options.options_path
+ for p in paths:
+ if os.path.isfile(os.path.join(p, optfilename)):
+ optfilename = os.path.join(p, optfilename)
+ if options.verbose:
+ sys.stderr.write('Reading options from ' + optfilename + '\n')
+ Globals.separate_options = read_options_file(open(optfilename, "rU"))
+ break
+ else:
+ # If we are given a full filename and it does not exist, give an error.
+ # However, don't give error when we automatically look for .options file
+ # with the same name as .proto.
+ if options.verbose or had_abspath:
+ sys.stderr.write('Options file not found: ' + optfilename + '\n')
+ Globals.separate_options = []
+
+ Globals.matched_namemasks = set()
+
+ # Parse the file
+ file_options = get_nanopb_suboptions(fdesc, toplevel_options, Names([filename]))
+ f = ProtoFile(fdesc, file_options)
+ f.optfilename = optfilename
+
+ return f
+
+def process_file(filename, fdesc, options, other_files = {}):
+ '''Process a single file.
+ filename: The full path to the .proto or .pb source file, as string.
+ fdesc: The loaded FileDescriptorSet, or None to read from the input file.
+ options: Command line options as they come from OptionsParser.
+
+ Returns a dict:
+ {'headername': Name of header file,
+ 'headerdata': Data for the .h header file,
+ 'sourcename': Name of the source code file,
+ 'sourcedata': Data for the .c source code file
+ }
+ '''
+ f = parse_file(filename, fdesc, options)
+
+ # Provide dependencies if available
+ for dep in f.fdesc.dependency:
+ if dep in other_files:
+ f.add_dependency(other_files[dep])
+
+ # Decide the file names
+ noext = os.path.splitext(filename)[0]
+ headername = noext + options.extension + '.h'
+ sourcename = noext + options.extension + '.c'