123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- #!/usr/bin/env python3
- # This code runs compatibly under Python 2 and 3.x for x >= 2.
- # Preserve this property!
- """Generate AIS code from text file.
- This tool is intended to automate away the drudgery in bring up support
- for a new AIS message type. It parses the tabular description of a message
- and generates various useful code snippets from that. It can also be used to
- correct offsets in the tables themselves.
- Requires the AIVDM.txt file on standard input. Takes a single argument,
- which must match a string in a //: Type comment. Things you can generate:
- * -t: A corrected version of the table. It will redo all the offsets to be
- in conformance with the bit widths. (The other options rely only on the
- bit widths). If the old and new tables are different, an error message
- describing the corrections will be emitted to standard error.
- * -s: A structure definition capturing the message info, with member
- names extracted from the table and types computed from it.
- * -c: Bit-extraction code for the AIVDM driver. Grind out the right sequence
- of UBITS, SBITS, and UCHARS macros, and assignments to structure members,
- guaranteed correct if the table offsets and widths are.
- * -d: Code to dump the contents of the unpacked message structure as JSON. If
- the structure has float members, you'll get an if/then/else guarded by
- the scaled flag.
- * -r: A Python initializer stanza for jsongen.py, which is in turn used to
- generate the specification structure for a JSON parse that reads JSON
- into an instance of the message structure.
- * -a: Generate all of -s, -d, -c, and -r, and -t, not to stdout but to
- files named with 'tablegen' as a distinguishing part of the stem.
- The stem name can be overridden with the -o option.
- This generates almost all the code required to support a new message type.
- It's not quite "Look, ma, no handhacking!" You'll need to add default
- values to the Python stanza. If the structure definition contains character
- arrays, you'll have to fill in the dimensions by hand. You'll need to add
- a bit of glue to ais_json.c so that json_ais_read() actually calls the parser
- handing it the specification structure as a control argument.
- The -a, -c, -s, -d, and -r modes all take an argument, which should be a
- structure reference prefix to be prepended (before a dot) to each fieldname.
- Usually you'll need this to look something like "ais->typeN", but it could be
- "ais->typeN.FOO" if the generated code has to operate on a union member
- inside a type 6 or 8, or something similar.
- The -S and -E options allow you to generate code only for a specified span
- of fields in the table. This may be useful for dealing with groups of
- messages that have a common head section.
- This code interprets magic comments in the input
- //: Type
- The token following "Type" is the name of the table
- //: xxxx vocabulary
- A subtable describing a controlled vocabulary for field xxxx in the
- preceding table.
- TO-DO: generate code for ais.py.
- """
- from __future__ import absolute_import, print_function, division
- import getopt
- import sys
- def correct_table(wfp):
- """Writes the corrected table."""
- print("Total bits:", base, file=sys.stderr)
- for (idx, t) in enumerate(table):
- if offsets[idx].strip():
- print("|" + offsets[idx] + t[owidth+1:].rstrip(), file=wfp)
- else:
- print(t.rstrip(), file=wfp)
- def make_driver_code(wfp):
- """Writes calls to bit-extraction macros.
- Requires UBITS, SBITS, UCHARS to act as they do in the AIVDM driver.
- Also relies on bitlen to be the message bit length, and i to be
- available as abn index variable."""
- record = after is None
- arrayname = None
- lbase = '\t'
- step = " " * 4
- indent = lbase
- last = 0
- for (i, t) in enumerate(table):
- if '|' in t:
- filds = [s.strip() for s in t.split('|')]
- width = filds[2]
- name = filds[4]
- ftype = filds[5]
- if after == name:
- record = True
- continue
- if before == name:
- record = False
- continue
- if not record:
- continue
- if ftype == 'x':
- print("\t/* skip %s bit%s */" % (width,
- ["", "s"][width > '1']), file=wfp)
- continue
- if ftype[0] == 'a':
- arrayname = name
- explicit = ftype[1] == '^'
- print('#define ARRAY_BASE %s' % offsets[i].strip(), file=wfp)
- print('#define ELEMENT_SIZE %s' % trailing, file=wfp)
- if explicit:
- lengthfield = last
- print(indent + "for (i = 0; i < %s; i++) {" % lengthfield,
- file=wfp)
- else:
- lengthfield = "n" + arrayname
- print(indent + "for (i = 0; ARRAY_BASE + "
- "(ELEMENT_SIZE*i) < bitlen; i++) {", file=wfp)
- indent += step
- print(indent + "int a = ARRAY_BASE + (ELEMENT_SIZE*i);",
- file=wfp)
- continue
- offset = offsets[i].split('-')[0]
- if arrayname:
- target = "%s.%s[i].%s" % (structnme, arrayname, name)
- offset = "a + " + offset
- else:
- target = "%s.%s" % (structname, name)
- if ftype[0].lower() in ('u', 'i', 'e'):
- print(indent + "%s\t= %sBITS(%s, %s);" %
- (target,
- {'u': 'U', 'e': 'U', 'i': 'S'}[ftype[0].lower()],
- offset, width), file=wfp)
- elif ftype == 't':
- print(indent + "UCHARS(%s, %s);" % (offset, target), file=wfp)
- elif ftype == 'b':
- print(indent + "%s\t= (bool)UBITS(%s, 1);" % (target, offset),
- file=wfp)
- else:
- print(indent + "/* %s bits of type %s */" %
- (width, ftype), file=wfp)
- last = name
- if arrayname:
- indent = lbase
- print(indent + "}", file=wfp)
- if not explicit:
- print(indent + "%s.%s = ind;" %
- (structname, lengthfield), file=wfp)
- print("#undef ARRAY_BASE", file=wfp)
- print("#undef ELEMENT_SIZE", file=wfp)
- def make_structure(wfp):
- """Write a structure definition corresponding to the table."""
- global structname
- record = after is None
- last = 0
- baseindent = 8
- step = 4
- inwards = step
- arrayname = None
- def tabify(n):
- """convert to tabs."""
- return ('\t' * (n // 8)) + (" " * (n % 8))
- print(tabify(baseindent) + "struct {", file=wfp)
- for (i, t) in enumerate(table):
- if '|' in t:
- fields = [s.strip() for s in t.split('|')]
- width = fields[2]
- description = fields[3].strip()
- name = fields[4]
- ftype = fields[5]
- if after == name:
- record = True
- continue
- if before == name:
- record = False
- continue
- if ftype == 'x' or not record:
- continue
- if ftype[0] == 'a':
- arrayname = name
- if ftype[1] == '^':
- lengthfield = last
- ftype = ftype[1:]
- else:
- lengthfield = "n%s" % arrayname
- print(tabify(baseindent + inwards) +
- "signed int %s;" % lengthfield, file=wfp)
- if arrayname.endswith("s"):
- typename = arrayname[:-1]
- else:
- typename = arrayname
- print(tabify(baseindent + inwards) + "struct %s_t {" %
- typename, file=wfp)
- inwards += step
- arraydim = ftype[1:]
- continue
- elif ftype == 'u' or ftype == 'e' or ftype[0] == 'U':
- decl = "unsigned int %s;\t/* %s */" % (name, description)
- elif ftype == 'i' or ftype[0] == 'I':
- decl = "signed int %s;\t/* %s */" % (name, description)
- elif ftype == 'b':
- decl = "bool %s;\t/* %s */" % (name, description)
- elif ftype == 't':
- stl = int(width) // 6
- decl = "char %s[%d+1];\t/* %s */" % (name, stl, description)
- else:
- decl = "/* %s bits of type %s */" % (width, ftype)
- print(tabify(baseindent + inwards) + decl, file=wfp)
- last = name
- if arrayname:
- inwards -= step
- print(tabify(baseindent + inwards) + "} %s[%s];"
- % (arrayname, arraydim), file=wfp)
- if "->" in structname:
- typename = structname.split("->")[1]
- if "." in typename:
- structname = structname.split(".")[1]
- print(tabify(baseindent) + "} %s;" % typename, file=wfp)
- def make_json_dumper(wfp):
- """Write the skeleton of a JSON dump corresponding to the table.
- Also, if there are subtables, some initializers."""
- last = 0
- if subtables:
- for (name, lines) in subtables:
- wfp.write(" const char *%s_vocabulary[] = {\n" % name)
- for ln in lines:
- value = ln[1]
- if value.endswith(" (default)"):
- value = value[:-10]
- wfp.write(' "%s",\n' % value)
- wfp.write(" };\n")
- wfp.write('#define DISPLAY_%s(n) (((n) < '
- '(unsigned int)NITEMS(%s_vocabulary)) ? '
- '%s_vocabulary[n] : "INVALID %s")\n' %
- (name.upper(), name, name, name.upper()))
- wfp.write("\n")
- record = after is None
- # Elements of each tuple type except 'a':
- # 1. variable name,
- # 2. unscaled printf format
- # 3. wrapper for unscaled variable reference
- # 4. scaled printf format
- # 5. wrapper for scaled variable reference
- # Elements of 'a' tuple:
- # 1. Name of array field
- # 2. None
- # 3. None
- # 4. None
- # 5. Name of length field
- tuples = []
- vocabularies = [x[0] for x in subtables]
- for (i, t) in enumerate(table):
- if '|' in t:
- fields = [s.strip() for s in t.split('|')]
- name = fields[4]
- ftype = fields[5]
- if after == name:
- record = True
- continue
- if before == name:
- record = False
- continue
- if ftype == 'x' or not record:
- continue
- fmt = r'\"%s\":' % name
- fmt_text = r'\"%s_text\":' % name
- if ftype == 'u':
- tuples.append((name,
- fmt+"%u", "%s",
- None, None))
- elif ftype == 'e':
- tuples.append((name,
- fmt+"%u", "%s",
- None, None))
- if vocabularies:
- this = vocabularies.pop(0)
- ref = "DISPLAY_%s(%%s)" % (this.upper())
- else:
- ref = 'FOO[%s]'
- tuples.append((name,
- fmt_text+r"\"%s\"", ref,
- None, None))
- elif ftype == 'i':
- tuples.append((name,
- fmt+"%d", "%s",
- None, None))
- elif ftype == 't':
- tuples.append((name,
- fmt+r'\"%s\"', "%s",
- None, None))
- elif ftype == 'b':
- tuples.append((name,
- fmt+r'\"%s\"', "JSON_BOOL(%s)",
- None, None))
- elif ftype[0] == 'd':
- print("Cannot generate code for data members", file=sys.stderr)
- sys.exit(1)
- elif ftype[0] == 'U':
- tuples.append((name,
- fmt+"%u", "%s",
- fmt+"%%.%sf" % ftype[1], '%s / SCALE'))
- elif ftype[0] == 'I':
- tuples.append((name,
- fmt+"%d", "%s",
- fmt+"%%.%sf" % ftype[1], '%s / SCALE'))
- elif ftype[0] == 'a':
- ftype = ftype[1:]
- if ftype[0] == '^':
- lengthfield = last
- else:
- lengthfield = "n" + name
- tuples.append((name, None, None, None, lengthfield))
- else:
- print("Unknown type code", ftype, file=sys.stderr)
- sys.exit(1)
- last = name
- startspan = 0
- def scaled(idx):
- """Check if scaled."""
- return tuples[idx][3] is not None
- def tslice(e, idx):
- """Missing docstring."""
- return [x[idx] for x in tuples[startspan:e+1]]
- lbase = " " * 8
- step = " " * 4
- inarray = None
- header = "(void)snprintf(buf + strlen(buf), buflen - strlen(buf),"
- for (i, (var, uf, uv, sf, sv)) in enumerate(tuples):
- if uf is not None:
- print(lbase + "for (i = 0; i < %s.%s; i++) {" % (structname, sv),
- file=wfp)
- inarray = var
- lbase = " " * 12
- startspan = i+1
- continue
- # At end of tuples, or if scaled flag changes, or if next op is array,
- # flush out dump code for a span of fields.
- if i+1 == len(tuples):
- endit = '}",'
- elif tuples[i+1][1] is not None:
- endit = r',\"%s\":[",' % tuples[i+1][0]
- elif scaled(i) != scaled(i + 1):
- endit = ',",'
- else:
- endit = None
- if endit:
- if not scaled(i):
- print(lbase + header, file=wfp)
- if inarray:
- prefix = '{"'
- else:
- prefix = '"'
- print(lbase + step + prefix + ','.join(tslice(i, 1)) + endit,
- file=wfp)
- for (j, t) in enumerate(tuples[startspan:i+1]):
- if inarray:
- ref = structname + "." + inarray + "[i]." + t[0]
- else:
- ref = structname + "." + t[0]
- wfp.write(lbase + step + t[2] % ref)
- if j == i - startspan:
- wfp.write(");\n")
- else:
- wfp.write(",\n")
- else:
- print(lbase + "if (scaled)", file=wfp)
- print(lbase + step + header, file=wfp)
- print(lbase + step * 2 + '"' + ','.join(tslice(i, 3)) + endit,
- file=wfp)
- for (j, t) in enumerate(tuples[startspan:i+1]):
- if inarray:
- ref = structname + "." + inarray + "[i]." + t[0]
- else:
- ref = structname + "." + t[0]
- wfp.write(lbase + step*2 + t[4] % ref)
- if j == i - startspan:
- wfp.write(");\n")
- else:
- wfp.write(",\n")
- print(lbase + "else", file=wfp)
- print(lbase + step + header, file=wfp)
- print(lbase + step * 2 + '"' + ','.join(tslice(i, 1)) + endit,
- file=wfp)
- for (j, t) in enumerate(tuples[startspan:i+1]):
- if inarray:
- ref = structname + "." + inarray + "[i]." + t[0]
- else:
- ref = structname + "." + t[0]
- wfp.write(lbase + step*2 + t[2] % ref)
- if j == i - startspan:
- wfp.write(");\n")
- else:
- wfp.write(",\n")
- startspan = i+1
- # If we were looking at a trailing array, close scope
- if inarray:
- lbase = " " * 8
- print(lbase + "}", file=wfp)
- print(lbase + "if (buf[strlen(buf)-1] == ',')", file=wfp)
- print(lbase + step + r"buf[strlen(buf)-1] = '\0';", file=wfp)
- print(lbase + "(void)strlcat(buf, \"]}\", buflen - strlen(buf));",
- file=wfp)
- def make_json_generator(wfp):
- """Write a stanza for jsongen.py.in describing how to generate a
- JSON parser initializer from this table. You need to fill in
- __INITIALIZER__ and default values after this is generated."""
- extra = ""
- last = 0
- arrayname = None
- record = after is None
- print('''\
- {
- "initname" : "__INITIALIZER__",
- "headers": ("AIS_HEADER",),
- "structname": "%s",
- "fieldmap":(
- # fieldname type default''' % (structname,), file=wfp)
- for (i, t) in enumerate(table):
- if '|' in t:
- fields = [s.strip() for s in t.split('|')]
- name = fields[4]
- ftype = fields[5]
- if after == name:
- record = True
- continue
- if before == name:
- record = False
- continue
- if ftype == 'x' or not record:
- continue
- if ftype[0] == 'a':
- arrayname = name
- if arrayname.endswith("s"):
- typename = arrayname[:-1]
- else:
- typename = arrayname
- readtype = 'array'
- dimension = ftype[1:]
- if dimension[0] == '^':
- lengthfield = last
- dimension = dimension[1:]
- else:
- lengthfield = "n" + arrayname
- extra = " " * 8
- print(" ('%s',%s 'array', (" %
- (arrayname, " "*(10-len(arrayname))), file=wfp)
- print(" ('%s_t', '%s', (" % (typename, lengthfield),
- file=wfp)
- else:
- # Depends on the assumption that the read code
- # always sees unscaled JSON.
- readtype = {
- 'u': "uinteger",
- 'U': "uinteger",
- 'e': "uinteger",
- 'i': "integer",
- 'I': "integer",
- 'b': "boolean",
- 't': "string",
- 'd': "string",
- }[ftype[0]]
- typedefault = {
- 'u': "'PUT_DEFAULT_HERE'",
- 'U': "'PUT_DEFAULT_HERE'",
- 'e': "'PUT DEFAULT HERE'",
- 'i': "'PUT_DEFAULT_HERE'",
- 'I': "'PUT_DEFAULT_HERE'",
- 'b': "\'false\'",
- 't': "None",
- }[ftype[0]]
- namedefaults = {
- "month": "'0'",
- "day": "'0'",
- "hour": "'24'",
- "minute": "'60'",
- "second": "'60'",
- }
- default = namedefaults.get(name) or typedefault
- print(extra + " ('%s',%s '%s',%s %s)," %
- (name, " "*(10-len(name)), readtype,
- " "*(8-len(readtype)), default), file=wfp)
- if ftype[0] == 'e':
- print(extra + " ('%s_text',%s'ignore', None)," %
- (name, " "*(6-len(name))), file=wfp)
- last = name
- if arrayname:
- print(" )))),", file=wfp)
- print(" ),", file=wfp)
- print(" },", file=wfp)
- if __name__ == '__main__':
- try:
- # FIXME: Convert to argparse
- (options, arguments) = getopt.getopt(sys.argv[1:], "a:tc:s:d:S:E:r:o:")
- except getopt.GetoptError as msg:
- print("tablecheck.py: " + str(msg))
- raise SystemExit(1)
- generate = maketable = makestruct = makedump = readgen = doall = False
- after = before = None
- filestem = "tablegen"
- for (switch, val) in options:
- if switch == '-a':
- doall = True
- structname = val
- elif switch == '-c':
- generate = True
- structname = val
- elif switch == '-s':
- makestruct = True
- structname = val
- elif switch == '-t':
- maketable = True
- elif switch == '-d':
- makedump = True
- structname = val
- elif switch == '-r':
- readgen = True
- structname = val
- elif switch == '-S':
- after = val
- elif switch == '-E':
- before = val
- elif switch == '-o':
- filestem = val
- if ((not generate and not maketable and not makestruct and
- not makedump and not readgen and not doall)):
- print("tablecheck.py: no mode selected", file=sys.stderr)
- sys.exit(1)
- # First, read in the table.
- # Sets the following:
- # table - the table lines
- # widths - array of table widths
- # ranges - array of table offsets
- # trailing - bit length of the table or trailing array element
- # subtables - list of following vocabulary tables.
- tablename = arguments[0]
- table = []
- ranges = []
- subtables = []
- state = 0
- for line in sys.stdin:
- if state == 0 and line.startswith("//: Type") and tablename in line:
- state = 1
- continue
- elif state == 1: # Found table tag
- if line.startswith("|="):
- state = 2
- continue
- elif state == 2: # Found table header
- if line.startswith("|="):
- state = 3
- continue
- elif line[0] == '|':
- fields = line.split("|")
- trailing = fields[1]
- ranges.append(fields[1].strip())
- fields[1] = " " * len(fields[1])
- line = "|".join(fields)
- else:
- ranges.append('')
- table.append(line)
- continue
- elif state == 3: # Found table end
- state = 4
- continue
- elif state == 4: # Skipping until subsidiary table
- if line.startswith("//:") and "vocabulary" in line:
- subtable_name = line.split()[1]
- subtable_content = []
- state = 5
- elif state == 5: # Seen subtable header
- if line.startswith("|="):
- state = 6
- continue
- elif state == 6: # Parsing subtable content
- if line.startswith("|="):
- subtables.append((subtable_name, subtable_content))
- state = 4
- continue
- elif line[0] == '|':
- subtable_content.append(
- [f.strip() for f in line[1:].strip().split("|")])
- continue
- if state == 0:
- print("Can't find named table.", file=sys.stderr)
- sys.exit(1)
- elif state < 3:
- print("Ill-formed table (in state %d)." % state, file=sys.stderr)
- sys.exit(1)
- table = table[1:]
- ranges = ranges[1:]
- widths = []
- for line in table:
- fields = line.split('|')
- if '|' not in line: # Continuation line
- widths.append('')
- elif fields[5][0] == 'a': # Array boundary indicator
- widths.append(None)
- else:
- widths.append(fields[2].strip())
- if '-' in trailing:
- trailing = trailing.split('-')[1]
- trailing = str(int(trailing)+1)
- # Compute offsets for an AIVDM message breakdown, given the bit widths.
- offsets = []
- base = 0
- corrections = False
- for w in widths:
- if w is None:
- offsets.append(repr(base))
- base = 0
- elif w == '':
- offsets.append('')
- else:
- w = int(w)
- offsets.append("%d-%d" % (base, base + w - 1))
- base += w
- if [p for p in zip(ranges, offsets) if p[0] != p[1]]:
- corrections = True
- print("Offset corrections:")
- for (old, new) in zip(ranges, offsets):
- if old != new:
- print(old, "->", new, file=sys.stderr)
- owidth = max(*list(map(len, offsets)))
- for (i, off) in enumerate(offsets):
- offsets[i] += " " * (owidth - len(offsets[i]))
- # Here's where we generate useful output.
- if doall:
- if corrections:
- correct_table(open(filestem + ".txt", "w"))
- make_driver_code(open(filestem + ".c", "w"))
- make_structure(open(filestem + ".h", "w"))
- make_json_dumper(open(filestem + "_json.c", "w"))
- make_json_generator(open(filestem + ".py", "w"))
- elif maketable:
- correct_table(sys.stdout)
- elif generate:
- make_driver_code(sys.stdout)
- elif makestruct:
- make_structure(sys.stdout)
- elif makedump:
- make_json_dumper(sys.stdout)
- elif readgen:
- make_json_generator(sys.stdout)
- # end
|