CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
Ardupilot

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: Ardupilot/ardupilot
Path: blob/master/Tools/autotest/param_metadata/param_parse.py
Views: 1799
1
#!/usr/bin/env python
2
3
'''Generates parameter metadata files suitable for consumption by
4
ground control stations and various web services
5
6
AP_FLAKE8_CLEAN
7
8
'''
9
10
from __future__ import print_function
11
import copy
12
import os
13
import re
14
import sys
15
from argparse import ArgumentParser
16
17
from param import (Library, Parameter, Vehicle, known_group_fields,
18
known_param_fields, required_param_fields, required_library_param_fields, known_units)
19
from htmlemit import HtmlEmit
20
from rstemit import RSTEmit
21
from rstlatexpdfemit import RSTLATEXPDFEmit
22
from xmlemit import XmlEmit
23
from mdemit import MDEmit
24
from jsonemit import JSONEmit
25
26
parser = ArgumentParser(description="Parse ArduPilot parameters.")
27
parser.add_argument("-v", "--verbose", dest='verbose', action='store_true', default=False, help="show debugging output")
28
parser.add_argument("--vehicle", required=True, help="Vehicle type to generate for")
29
parser.add_argument("--no-emit",
30
dest='emit_params',
31
action='store_false',
32
default=True,
33
help="don't emit parameter documention, just validate")
34
parser.add_argument("--format",
35
dest='output_format',
36
action='store',
37
default='all',
38
choices=['all', 'html', 'rst', 'rstlatexpdf', 'wiki', 'xml', 'json', 'edn', 'md'],
39
help="what output format to use")
40
41
args = parser.parse_args()
42
43
44
# Regular expressions for parsing the parameter metadata
45
46
prog_param = re.compile(r"@Param(?:{([^}]+)})?: (\w+).*((?:\n[ \t]*// @(\w+)(?:{([^}]+)})?: ?(.*))+)(?:\n[ \t\r]*\n|\n[ \t]+[A-Z]|\n\-\-\]\])", re.MULTILINE) # noqa
47
48
# match e.g @Value: 0=Unity, 1=Koala, 17=Liability
49
prog_param_fields = re.compile(r"[ \t]*// @(\w+): ?([^\r\n]*)")
50
# match e.g @Value{Copter}: 0=Volcano, 1=Peppermint
51
prog_param_tagged_fields = re.compile(r"[ \t]*// @(\w+){([^}]+)}: ([^\r\n]*)")
52
53
prog_groups = re.compile(r"@Group: *(\w+).*((?:\n[ \t]*// @(Path): (\S+))+)", re.MULTILINE)
54
55
apm_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../')
56
57
58
def find_vehicle_parameter_filepath(vehicle_name):
59
apm_tools_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../Tools/')
60
61
vehicle_name_to_dir_name_map = {
62
"Copter": "ArduCopter",
63
"Plane": "ArduPlane",
64
"Tracker": "AntennaTracker",
65
"Sub": "ArduSub",
66
}
67
68
# first try ArduCopter/Parameters.cpp
69
for top_dir in apm_path, apm_tools_path:
70
path = os.path.join(top_dir, vehicle_name, "Parameters.cpp")
71
if os.path.exists(path):
72
return path
73
74
# then see if we can map e.g. Copter -> ArduCopter
75
if vehicle_name in vehicle_name_to_dir_name_map:
76
path = os.path.join(top_dir, vehicle_name_to_dir_name_map[vehicle_name], "Parameters.cpp")
77
if os.path.exists(path):
78
return path
79
80
raise ValueError("Unable to find parameters file for (%s)" % vehicle_name)
81
82
83
def debug(str_to_print):
84
"""Debug output if verbose is set."""
85
if args.verbose:
86
print(str_to_print)
87
88
89
def lua_applets():
90
'''return list of Library objects for lua applets and drivers'''
91
lua_lib = Library("", reference="Lua Script", not_rst=True, check_duplicates=True)
92
dirs = ["libraries/AP_Scripting/applets", "libraries/AP_Scripting/drivers"]
93
paths = []
94
for d in dirs:
95
for root, dirs, files in os.walk(os.path.join(apm_path, d)):
96
for file in files:
97
if not file.endswith(".lua"):
98
continue
99
f = os.path.join(root, file)
100
debug("Adding lua path %s" % f)
101
# the library is expected to have the path as a relative path from within
102
# a vehicle directory
103
f = f.replace(apm_path, "../")
104
paths.append(f)
105
setattr(lua_lib, "Path", ','.join(paths))
106
return lua_lib
107
108
109
libraries = []
110
111
if args.vehicle != "AP_Periph":
112
# AP_Vehicle also has parameters rooted at "", but isn't referenced
113
# from the vehicle in any way:
114
ap_vehicle_lib = Library("", reference="VEHICLE") # the "" is tacked onto the front of param name
115
setattr(ap_vehicle_lib, "Path", os.path.join('..', 'libraries', 'AP_Vehicle', 'AP_Vehicle.cpp'))
116
libraries.append(ap_vehicle_lib)
117
118
libraries.append(lua_applets())
119
120
error_count = 0
121
current_param = None
122
current_file = None
123
124
125
def error(str_to_print):
126
"""Show errors."""
127
global error_count
128
error_count += 1
129
if current_file is not None:
130
print("Error in %s" % current_file)
131
if current_param is not None:
132
print("At param %s" % current_param)
133
print(str_to_print)
134
135
136
truename_map = {
137
"Rover": "Rover",
138
"ArduSub": "Sub",
139
"ArduCopter": "Copter",
140
"ArduPlane": "Plane",
141
"AntennaTracker": "Tracker",
142
"AP_Periph": "AP_Periph",
143
"Blimp": "Blimp",
144
}
145
valid_truenames = frozenset(truename_map.values())
146
truename = truename_map.get(args.vehicle, args.vehicle)
147
148
documentation_tags_which_are_comma_separated_nv_pairs = frozenset([
149
'Values',
150
'Bitmask',
151
])
152
153
vehicle_path = find_vehicle_parameter_filepath(args.vehicle)
154
155
basename = os.path.basename(os.path.dirname(vehicle_path))
156
path = os.path.normpath(os.path.dirname(vehicle_path))
157
reference = basename # so links don't break we use ArduCopter
158
vehicle = Vehicle(truename, path, reference=reference)
159
debug('Found vehicle type %s' % vehicle.name)
160
161
162
def process_vehicle(vehicle):
163
debug("===\n\n\nProcessing %s" % vehicle.name)
164
current_file = vehicle.path+'/Parameters.cpp'
165
166
f = open(current_file)
167
p_text = f.read()
168
f.close()
169
group_matches = prog_groups.findall(p_text)
170
171
debug(group_matches)
172
for group_match in group_matches:
173
lib = Library(group_match[0])
174
fields = prog_param_fields.findall(group_match[1])
175
for field in fields:
176
if field[0] in known_group_fields:
177
setattr(lib, field[0], field[1])
178
else:
179
error("group: unknown parameter metadata field '%s'" % field[0])
180
if not any(lib.name == parsed_l.name for parsed_l in libraries):
181
libraries.append(lib)
182
183
param_matches = []
184
param_matches = prog_param.findall(p_text)
185
186
for param_match in param_matches:
187
(only_vehicles, param_name, field_text) = (param_match[0],
188
param_match[1],
189
param_match[2])
190
if len(only_vehicles):
191
only_vehicles_list = [x.strip() for x in only_vehicles.split(",")]
192
for only_vehicle in only_vehicles_list:
193
if only_vehicle not in valid_truenames:
194
raise ValueError("Invalid only_vehicle %s" % only_vehicle)
195
if vehicle.truename not in only_vehicles_list:
196
continue
197
p = Parameter(vehicle.reference+":"+param_name, current_file)
198
debug(p.name + ' ')
199
global current_param
200
current_param = p.name
201
fields = prog_param_fields.findall(field_text)
202
p.__field_text = field_text
203
field_list = []
204
for field in fields:
205
(field_name, field_value) = field
206
field_list.append(field[0])
207
if field[0] in known_param_fields:
208
value = re.sub('@PREFIX@', "", field[1]).rstrip()
209
if hasattr(p, field_name):
210
if field_name in documentation_tags_which_are_comma_separated_nv_pairs:
211
# allow concatenation of (e.g.) bitmask fields
212
x = eval("p.%s" % field_name)
213
x += ", "
214
x += value
215
value = x
216
else:
217
error("%s already has field %s" % (p.name, field_name))
218
setattr(p, field[0], value)
219
elif field[0] in frozenset(["CopyFieldsFrom", "CopyValuesFrom"]):
220
setattr(p, field[0], field[1])
221
else:
222
error("param: unknown parameter metadata field '%s'" % field[0])
223
224
if (getattr(p, 'Values', None) is not None and
225
getattr(p, 'Bitmask', None) is not None):
226
error("Both @Values and @Bitmask present")
227
228
vehicle.params.append(p)
229
current_file = None
230
debug("Processed %u params" % len(vehicle.params))
231
232
233
process_vehicle(vehicle)
234
235
debug("Found %u documented libraries" % len(libraries))
236
237
libraries = list(libraries)
238
239
alllibs = libraries[:]
240
241
242
def process_library(vehicle, library, pathprefix=None):
243
'''process one library'''
244
paths = library.Path.split(',')
245
for path in paths:
246
path = path.strip()
247
global current_file
248
current_file = path
249
debug("\n Processing file '%s'" % path)
250
if pathprefix is not None:
251
libraryfname = os.path.join(pathprefix, path)
252
elif path.find('/') == -1:
253
libraryfname = os.path.join(vehicle.path, path)
254
else:
255
libraryfname = os.path.normpath(os.path.join(apm_path + '/libraries/' + path))
256
if path and os.path.exists(libraryfname):
257
f = open(libraryfname)
258
p_text = f.read()
259
f.close()
260
else:
261
error("Path %s not found for library %s (fname=%s)" % (path, library.name, libraryfname))
262
continue
263
264
param_matches = prog_param.findall(p_text)
265
debug("Found %u documented parameters" % len(param_matches))
266
for param_match in param_matches:
267
(only_vehicles, param_name, field_text) = (param_match[0],
268
param_match[1],
269
param_match[2])
270
if len(only_vehicles):
271
only_vehicles_list = [x.strip() for x in only_vehicles.split(",")]
272
for only_vehicle in only_vehicles_list:
273
if only_vehicle not in valid_truenames:
274
raise ValueError("Invalid only_vehicle %s" % only_vehicle)
275
if vehicle.name not in only_vehicles_list:
276
continue
277
p = Parameter(library.name+param_name, current_file)
278
debug(p.name + ' ')
279
global current_param
280
current_param = p.name
281
fields = prog_param_fields.findall(field_text)
282
p.__field_text = field_text
283
field_list = []
284
for field in fields:
285
(field_name, field_value) = field
286
field_list.append(field[0])
287
if field[0] in known_param_fields:
288
value = re.sub('@PREFIX@', library.name, field[1])
289
if hasattr(p, field_name):
290
if field_name in documentation_tags_which_are_comma_separated_nv_pairs:
291
# allow concatenation of (e.g.) bitmask fields
292
x = eval("p.%s" % field_name)
293
x += ", "
294
x += value
295
value = x
296
else:
297
error("%s already has field %s" % (p.name, field_name))
298
setattr(p, field[0], value)
299
elif field[0] in frozenset(["CopyFieldsFrom", "CopyValuesFrom"]):
300
setattr(p, field[0], field[1])
301
else:
302
error("param: unknown parameter metadata field %s" % field[0])
303
304
debug("matching %s" % field_text)
305
fields = prog_param_tagged_fields.findall(field_text)
306
# a parameter is considered to be vehicle-specific if
307
# there does not exist a Values: or Values{VehicleName}
308
# for that vehicle but @Values{OtherVehicle} exists.
309
seen_values_or_bitmask_for_this_vehicle = False
310
seen_values_or_bitmask_for_other_vehicle = False
311
for field in fields:
312
only_for_vehicles = field[1].split(",")
313
only_for_vehicles = [some_vehicle.rstrip().lstrip() for some_vehicle in only_for_vehicles]
314
delta = set(only_for_vehicles) - set(truename_map.values())
315
if len(delta):
316
error("Unknown vehicles (%s)" % delta)
317
debug("field[0]=%s vehicle=%s field[1]=%s only_for_vehicles=%s\n" %
318
(field[0], vehicle.name, field[1], str(only_for_vehicles)))
319
if field[0] not in known_param_fields:
320
error("tagged param: unknown parameter metadata field '%s'" % field[0])
321
continue
322
if vehicle.name not in only_for_vehicles:
323
if len(only_for_vehicles) and field[0] in documentation_tags_which_are_comma_separated_nv_pairs:
324
seen_values_or_bitmask_for_other_vehicle = True
325
continue
326
327
append_value = False
328
if field[0] in documentation_tags_which_are_comma_separated_nv_pairs:
329
if vehicle.name in only_for_vehicles:
330
if seen_values_or_bitmask_for_this_vehicle:
331
append_value = hasattr(p, field[0])
332
seen_values_or_bitmask_for_this_vehicle = True
333
else:
334
if seen_values_or_bitmask_for_this_vehicle:
335
continue
336
append_value = hasattr(p, field[0])
337
338
value = re.sub('@PREFIX@', library.name, field[2])
339
if append_value:
340
setattr(p, field[0], getattr(p, field[0]) + ',' + value)
341
else:
342
setattr(p, field[0], value)
343
344
if (getattr(p, 'Values', None) is not None and
345
getattr(p, 'Bitmask', None) is not None):
346
error("Both @Values and @Bitmask present")
347
348
if (getattr(p, 'Values', None) is None and
349
getattr(p, 'Bitmask', None) is None):
350
# values and Bitmask available for this vehicle
351
if seen_values_or_bitmask_for_other_vehicle:
352
# we've (e.g.) seen @Values{Copter} when we're
353
# processing for Rover, and haven't seen either
354
# @Values: or @Vales{Rover} - so we omit this
355
# parameter on the assumption that it is not
356
# applicable for this vehicle.
357
continue
358
359
if getattr(p, 'Vector3Parameter', None) is not None:
360
params_to_add = []
361
for axis in 'X', 'Y', 'Z':
362
new_p = copy.copy(p)
363
new_p.change_name(p.name + "_" + axis)
364
for a in ["Description"]:
365
if hasattr(new_p, a):
366
current = getattr(new_p, a)
367
setattr(new_p, a, current + " (%s-axis)" % axis)
368
params_to_add.append(new_p)
369
else:
370
params_to_add = [p]
371
372
for p in params_to_add:
373
p.path = path # Add path. Later deleted - only used for duplicates
374
if library.check_duplicates and library.has_param(p.name):
375
error("Duplicate parameter %s in %s" % (p.name, library.name))
376
continue
377
library.params.append(p)
378
379
group_matches = prog_groups.findall(p_text)
380
debug("Found %u groups" % len(group_matches))
381
debug(group_matches)
382
done_groups = dict()
383
for group_match in group_matches:
384
group = group_match[0]
385
debug("Group: %s" % group)
386
do_append = True
387
if group in done_groups:
388
# this is to handle cases like the RangeFinder
389
# parameters, where the wasp stuff gets tack into the
390
# same RNGFND1_ group
391
lib = done_groups[group]
392
do_append = False
393
else:
394
lib = Library(group)
395
done_groups[group] = lib
396
397
fields = prog_param_fields.findall(group_match[1])
398
for field in fields:
399
if field[0] in known_group_fields:
400
setattr(lib, field[0], field[1])
401
elif field[0] in ["CopyFieldsFrom", "CopyValuesFrom"]:
402
setattr(p, field[0], field[1])
403
else:
404
error("unknown parameter metadata field '%s'" % field[0])
405
if not any(lib.name == parsed_l.name for parsed_l in libraries):
406
if do_append:
407
lib.set_name(library.name + lib.name)
408
debug("Group name: %s" % lib.name)
409
process_library(vehicle, lib, os.path.dirname(libraryfname))
410
if do_append:
411
alllibs.append(lib)
412
413
current_file = None
414
415
416
for library in libraries:
417
debug("===\n\n\nProcessing library %s" % library.name)
418
419
if hasattr(library, 'Path'):
420
process_library(vehicle, library)
421
else:
422
error("Skipped: no Path found")
423
424
debug("Processed %u documented parameters" % len(library.params))
425
426
# sort libraries by name
427
alllibs = sorted(alllibs, key=lambda x: x.name)
428
429
libraries = alllibs
430
431
432
def is_number(numberString):
433
try:
434
float(numberString)
435
return True
436
except ValueError:
437
return False
438
439
440
def clean_param(param):
441
if (hasattr(param, "Values")):
442
valueList = param.Values.split(",")
443
new_valueList = []
444
for i in valueList:
445
(start, sep, end) = i.partition(":")
446
if sep != ":":
447
raise ValueError("Expected a colon separator in (%s)" % (i,))
448
if len(end) == 0:
449
raise ValueError("Expected a colon-separated string, got (%s)" % i)
450
end = end.strip()
451
start = start.strip()
452
new_valueList.append(":".join([start, end]))
453
param.Values = ",".join(new_valueList)
454
455
if hasattr(param, "Vector3Parameter"):
456
delattr(param, "Vector3Parameter")
457
458
459
def do_copy_values(vehicle_params, libraries, param):
460
if not hasattr(param, "CopyValuesFrom"):
461
return
462
463
# so go and find the values...
464
wanted_name = param.CopyValuesFrom
465
if hasattr(param, 'Vector3Parameter'):
466
suffix = param.name[-2:]
467
wanted_name += suffix
468
469
del param.CopyValuesFrom
470
for x in vehicle_params:
471
name = x.name
472
(v, name) = name.split(":")
473
if name != wanted_name:
474
continue
475
param.Values = x.Values
476
return
477
478
for lib in libraries:
479
for x in lib.params:
480
if x.name != wanted_name:
481
continue
482
param.Values = x.Values
483
return
484
485
error("Did not find value to copy (%s wants %s)" %
486
(param.name, wanted_name))
487
488
489
def do_copy_fields(vehicle_params, libraries, param):
490
do_copy_values(vehicle_params, libraries, param)
491
492
if not hasattr(param, 'CopyFieldsFrom'):
493
return
494
495
# so go and find the values...
496
wanted_name = param.CopyFieldsFrom
497
del param.CopyFieldsFrom
498
499
if hasattr(param, 'Vector3Parameter'):
500
suffix = param.name[-2:]
501
wanted_name += suffix
502
503
for x in vehicle_params:
504
name = x.name
505
(v, name) = name.split(":")
506
if name != wanted_name:
507
continue
508
for field in dir(x):
509
if hasattr(param, field):
510
# override
511
continue
512
if field.startswith("__") or field in frozenset(["name", "real_path"]):
513
# internal methods like __ne__
514
continue
515
setattr(param, field, getattr(x, field))
516
return
517
518
for lib in libraries:
519
for x in lib.params:
520
if x.name != wanted_name:
521
continue
522
for field in dir(x):
523
if hasattr(param, field):
524
# override
525
continue
526
if field.startswith("__") or field in frozenset(["name", "real_path"]):
527
# internal methods like __ne__
528
continue
529
setattr(param, field, getattr(x, field))
530
return
531
532
error("Did not find value to copy (%s wants %s)" %
533
(param.name, wanted_name))
534
535
536
def validate(param, is_library=False):
537
"""
538
Validates the parameter meta data.
539
"""
540
global current_file
541
current_file = param.real_path
542
global current_param
543
current_param = param.name
544
# Validate values
545
if (hasattr(param, "Range")):
546
rangeValues = param.__dict__["Range"].split(" ")
547
if (len(rangeValues) != 2):
548
error("Invalid Range values for %s (%s)" %
549
(param.name, param.__dict__["Range"]))
550
return
551
min_value = rangeValues[0]
552
max_value = rangeValues[1]
553
if not is_number(min_value):
554
error("Min value not number: %s %s" % (param.name, min_value))
555
return
556
if not is_number(max_value):
557
error("Max value not number: %s %s" % (param.name, max_value))
558
return
559
# Check for duplicate in @value field
560
if (hasattr(param, "Values")):
561
valueList = param.__dict__["Values"].split(",")
562
values = []
563
for i in valueList:
564
i = i.replace(" ", "")
565
values.append(i.partition(":")[0])
566
if (len(values) != len(set(values))):
567
error("Duplicate values found" + str({x for x in values if values.count(x) > 1}))
568
# Validate units
569
if (hasattr(param, "Units")):
570
if (param.__dict__["Units"] != "") and (param.__dict__["Units"] not in known_units):
571
error("unknown units field '%s'" % param.__dict__["Units"])
572
# Validate User
573
if (hasattr(param, "User")):
574
if param.User.strip() not in ["Standard", "Advanced"]:
575
error("unknown user (%s)" % param.User.strip())
576
577
if (hasattr(param, "Description")):
578
if not param.Description or not param.Description.strip():
579
error("Empty Description (%s)" % param)
580
581
required_fields = required_param_fields
582
if is_library:
583
required_fields = required_library_param_fields
584
for req_field in required_fields:
585
if not getattr(param, req_field, False):
586
error("missing parameter metadata field '%s' in %s" % (req_field, param.__field_text))
587
588
589
# handle CopyFieldsFrom and CopyValuesFrom:
590
for param in vehicle.params:
591
do_copy_fields(vehicle.params, libraries, param)
592
for library in libraries:
593
for param in library.params:
594
do_copy_fields(vehicle.params, libraries, param)
595
596
for param in vehicle.params:
597
clean_param(param)
598
599
for param in vehicle.params:
600
validate(param)
601
602
# Find duplicate names in library and fix up path
603
for library in libraries:
604
param_names_seen = set()
605
param_names_duplicate = set()
606
# Find duplicates:
607
for param in library.params:
608
if param.name in param_names_seen: # is duplicate
609
param_names_duplicate.add(param.name)
610
param_names_seen.add(param.name)
611
# Fix up path for duplicates
612
for param in library.params:
613
if param.name in param_names_duplicate:
614
param.path = param.path.rsplit('/')[-1].rsplit('.')[0]
615
else:
616
# not a duplicate, so delete attribute.
617
delattr(param, "path")
618
619
for library in libraries:
620
for param in library.params:
621
clean_param(param)
622
623
for library in libraries:
624
for param in library.params:
625
validate(param, is_library=True)
626
627
if not args.emit_params:
628
sys.exit(error_count)
629
630
all_emitters = {
631
'json': JSONEmit,
632
'xml': XmlEmit,
633
'html': HtmlEmit,
634
'rst': RSTEmit,
635
'rstlatexpdf': RSTLATEXPDFEmit,
636
'md': MDEmit,
637
}
638
639
try:
640
from ednemit import EDNEmit
641
all_emitters['edn'] = EDNEmit
642
except ImportError:
643
# if the user wanted edn only then don't hide any errors
644
if args.output_format == 'edn':
645
raise
646
647
if args.verbose:
648
print("Unable to emit EDN, install edn_format and pytz if edn is desired")
649
650
# filter to just the ones we want to emit:
651
emitters_to_use = []
652
for emitter_name in all_emitters.keys():
653
if args.output_format == 'all' or args.output_format == emitter_name:
654
emitters_to_use.append(emitter_name)
655
656
# actually invoke each emitter:
657
for emitter_name in emitters_to_use:
658
emit = all_emitters[emitter_name]()
659
660
emit.emit(vehicle)
661
662
emit.start_libraries()
663
664
# create a single parameter list for all SIM_ parameters (for rst to use)
665
sim_params = []
666
for library in libraries:
667
if library.name.startswith("SIM_"):
668
sim_params.extend(library.params)
669
sim_params = sorted(sim_params, key=lambda x : x.name)
670
671
for library in libraries:
672
if library.params:
673
# we sort the parameters in the SITL library to avoid
674
# rename, and on the assumption that an asciibetical sort
675
# gives a good layout:
676
if emitter_name == 'rst':
677
if library.not_rst:
678
continue
679
if library.name == 'SIM_':
680
library = copy.deepcopy(library)
681
library.params = sim_params
682
elif library.name.startswith('SIM_'):
683
continue
684
emit.emit(library)
685
686
emit.close()
687
688
sys.exit(error_count)
689
690