fprime-gds 4.0.2a3__py3-none-any.whl → 4.0.2a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. fprime_gds/common/distributor/distributor.py +9 -6
  2. fprime_gds/common/fpy/README.md +190 -42
  3. fprime_gds/common/fpy/SPEC.md +153 -39
  4. fprime_gds/common/fpy/bytecode/assembler.py +62 -0
  5. fprime_gds/common/fpy/bytecode/directives.py +620 -294
  6. fprime_gds/common/fpy/codegen.py +687 -910
  7. fprime_gds/common/fpy/grammar.lark +44 -9
  8. fprime_gds/common/fpy/main.py +27 -4
  9. fprime_gds/common/fpy/model.py +799 -0
  10. fprime_gds/common/fpy/parser.py +29 -34
  11. fprime_gds/common/fpy/test_helpers.py +119 -0
  12. fprime_gds/common/fpy/types.py +563 -0
  13. fprime_gds/executables/cli.py +9 -0
  14. fprime_gds/executables/run_deployment.py +2 -0
  15. fprime_gds/flask/app.py +17 -2
  16. fprime_gds/flask/default_settings.py +1 -0
  17. fprime_gds/flask/static/index.html +7 -4
  18. fprime_gds/flask/static/js/config.js +9 -1
  19. fprime_gds/flask/static/js/vue-support/channel.js +16 -0
  20. fprime_gds/flask/static/js/vue-support/event.js +1 -0
  21. fprime_gds/flask/static/js/vue-support/fp-row.js +25 -1
  22. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/METADATA +1 -1
  23. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/RECORD +28 -24
  24. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/entry_points.txt +2 -1
  25. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/WHEEL +0 -0
  26. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/licenses/LICENSE.txt +0 -0
  27. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/licenses/NOTICE.txt +0 -0
  28. {fprime_gds-4.0.2a3.dist-info → fprime_gds-4.0.2a4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,563 @@
1
+ from __future__ import annotations
2
+ from abc import ABC
3
+ import inspect
4
+ from dataclasses import dataclass, field, fields
5
+ import struct
6
+ import traceback
7
+ import typing
8
+ from typing import Union, get_args, get_origin
9
+
10
+ # In Python 3.10+, the `|` operator creates a `types.UnionType`.
11
+ # We need to handle this for forward compatibility, but it won't exist in 3.9.
12
+ try:
13
+ from types import UnionType
14
+
15
+ UNION_TYPES = (Union, UnionType)
16
+ except ImportError:
17
+ UNION_TYPES = (Union,)
18
+
19
+ from fprime_gds.common.fpy.bytecode.directives import (
20
+ StackOpDirective,
21
+ FloatLogDirective,
22
+ Directive,
23
+ ExitDirective,
24
+ WaitAbsDirective,
25
+ WaitRelDirective,
26
+ )
27
+ from fprime_gds.common.templates.ch_template import ChTemplate
28
+ from fprime_gds.common.templates.cmd_template import CmdTemplate
29
+ from fprime_gds.common.templates.prm_template import PrmTemplate
30
+ from fprime.common.models.serialize.time_type import TimeType
31
+ from fprime.common.models.serialize.serializable_type import SerializableType
32
+ from fprime.common.models.serialize.array_type import ArrayType
33
+ from fprime.common.models.serialize.numerical_types import (
34
+ U32Type,
35
+ U16Type,
36
+ U64Type,
37
+ U8Type,
38
+ I16Type,
39
+ I32Type,
40
+ I64Type,
41
+ I8Type,
42
+ F32Type,
43
+ F64Type,
44
+ FloatType,
45
+ IntegerType,
46
+ )
47
+ from fprime.common.models.serialize.string_type import StringType
48
+ from fprime.common.models.serialize.bool_type import BoolType
49
+ from fprime_gds.common.fpy.parser import (
50
+ AstExpr,
51
+ AstOp,
52
+ AstReference,
53
+ Ast,
54
+ AstAssign,
55
+ )
56
+ from fprime.common.models.serialize.type_base import BaseType as FppType
57
+
58
+ MAX_DIRECTIVES_COUNT = 1024
59
+ MAX_DIRECTIVE_SIZE = 2048
60
+ MAX_STACK_SIZE = 65535
61
+
62
+ COMPILER_MAX_STRING_SIZE = 128
63
+
64
+
65
+ # this is the "internal" integer type that integer literals have by
66
+ # default. it is arbitrary precision
67
+ class InternalIntType(IntegerType):
68
+ @classmethod
69
+ def range(cls):
70
+ raise NotImplementedError()
71
+
72
+ @staticmethod
73
+ def get_serialize_format():
74
+ raise NotImplementedError()
75
+
76
+ @classmethod
77
+ def get_bits(cls):
78
+ raise NotImplementedError()
79
+
80
+ @classmethod
81
+ def validate(cls, val):
82
+ if not isinstance(val, int):
83
+ raise RuntimeError()
84
+
85
+
86
+ InternalStringType = StringType.construct_type("InternalStringType", None)
87
+
88
+
89
+ SPECIFIC_NUMERIC_TYPES = (
90
+ U32Type,
91
+ U16Type,
92
+ U64Type,
93
+ U8Type,
94
+ I16Type,
95
+ I32Type,
96
+ I64Type,
97
+ I8Type,
98
+ F32Type,
99
+ F64Type,
100
+ )
101
+ SPECIFIC_INTEGER_TYPES = (
102
+ U32Type,
103
+ U16Type,
104
+ U64Type,
105
+ U8Type,
106
+ I16Type,
107
+ I32Type,
108
+ I64Type,
109
+ I8Type,
110
+ )
111
+ SIGNED_INTEGER_TYPES = (
112
+ I16Type,
113
+ I32Type,
114
+ I64Type,
115
+ I8Type,
116
+ )
117
+ UNSIGNED_INTEGER_TYPES = (
118
+ U32Type,
119
+ U16Type,
120
+ U64Type,
121
+ U8Type,
122
+ )
123
+ SPECIFIC_FLOAT_TYPES = (
124
+ F32Type,
125
+ F64Type,
126
+ )
127
+
128
+
129
+ def is_instance_compat(obj, cls):
130
+ """
131
+ A wrapper for isinstance() that correctly handles Union types in Python 3.9+.
132
+
133
+ Args:
134
+ obj: The object to check.
135
+ cls: The class, tuple of classes, or Union type to check against.
136
+
137
+ Returns:
138
+ True if the object is an instance of the class or any type in the Union.
139
+ """
140
+ origin = get_origin(cls)
141
+ if origin in UNION_TYPES:
142
+ # It's a Union type, so get its arguments.
143
+ # e.g., get_args(Union[int, str]) returns (int, str)
144
+ return isinstance(obj, get_args(cls))
145
+
146
+ # It's not a Union, so it's a regular type (like int) or a
147
+ # tuple of types ((int, str)), which isinstance handles natively.
148
+ return isinstance(obj, cls)
149
+
150
+
151
+ # a value of type FppTypeClass is a Python `type` object representing
152
+ # the type of an Fprime value
153
+ FppTypeClass = type[FppType]
154
+
155
+
156
+ class NothingType(ABC):
157
+ """a type which has no valid values in fprime. used to denote
158
+ a function which doesn't return a value"""
159
+
160
+ @classmethod
161
+ def __subclasscheck__(cls, subclass):
162
+ return False
163
+
164
+
165
+ # the `type` object representing the NothingType class
166
+ NothingTypeClass = type[NothingType]
167
+
168
+
169
+ class CompileException(BaseException):
170
+ def __init__(self, msg, node: Ast):
171
+ self.msg = msg
172
+ self.node = node
173
+ self.stack_trace = "\n".join(traceback.format_stack(limit=8)[:-1])
174
+
175
+ def __str__(self):
176
+ if self.node is not None:
177
+ return f'{self.stack_trace}\nAt line {self.node.meta.line} "{self.node.node_text}": {self.msg}'
178
+ return f"{self.stack_trace}\n{self.msg}"
179
+
180
+
181
+ @dataclass
182
+ class FpyCallable:
183
+ return_type: FppTypeClass | NothingTypeClass
184
+ args: list[tuple[str, FppTypeClass]]
185
+
186
+
187
+ @dataclass
188
+ class FpyCmd(FpyCallable):
189
+ cmd: CmdTemplate
190
+
191
+
192
+ @dataclass
193
+ class FpyMacro(FpyCallable):
194
+ dir: type[Directive]
195
+ """a function which instantiates the macro given the argument exprs"""
196
+
197
+
198
+ MACROS: dict[str, FpyMacro] = {
199
+ "sleep": FpyMacro(
200
+ NothingType,
201
+ [
202
+ (
203
+ "seconds",
204
+ U32Type,
205
+ ),
206
+ ("microseconds", U32Type),
207
+ ],
208
+ WaitRelDirective,
209
+ ),
210
+ "sleep_until": FpyMacro(NothingType, [("wakeup_time", TimeType)], WaitAbsDirective),
211
+ "exit": FpyMacro(NothingType, [("success", BoolType)], ExitDirective),
212
+ "log": FpyMacro(F64Type, [("operand", F64Type)], FloatLogDirective),
213
+ }
214
+
215
+
216
+ @dataclass
217
+ class FpyTypeCtor(FpyCallable):
218
+ type: FppTypeClass
219
+
220
+
221
+ @dataclass
222
+ class FieldReference:
223
+ """a reference to a field/index of an fprime type"""
224
+
225
+ parent: "FpyReference"
226
+ """the qualifier"""
227
+ type: FppTypeClass
228
+ """the fprime type of this reference"""
229
+ offset: int
230
+ """the constant offset in the parent type at which to find this field"""
231
+ name: str = None
232
+ """the name of the field, if applicable"""
233
+ idx: int = None
234
+ """the index of the field, if applicable"""
235
+
236
+ def get_from(self, parent_val: FppType) -> FppType:
237
+ """gets the field value from the parent value"""
238
+ assert isinstance(parent_val, self.type)
239
+ assert self.name is not None or self.idx is not None
240
+ value = None
241
+ if self.name is not None:
242
+ if isinstance(parent_val, SerializableType):
243
+ value = parent_val.val[self.name]
244
+ elif isinstance(parent_val, TimeType):
245
+ if self.name == "seconds":
246
+ value = parent_val.__secs
247
+ elif self.name == "useconds":
248
+ value = parent_val.__usecs
249
+ elif self.name == "time_base":
250
+ value = parent_val.__timeBase
251
+ elif self.name == "time_context":
252
+ value = parent_val.__timeContext
253
+ else:
254
+ assert False, self.name
255
+ else:
256
+ assert False, parent_val
257
+
258
+ else:
259
+
260
+ assert isinstance(parent_val, ArrayType), parent_val
261
+
262
+ value = parent_val._val[self.idx]
263
+
264
+ assert isinstance(value, self.type), (value, self.type)
265
+ return value
266
+
267
+
268
+ # named variables can be tlm chans, prms, callables, or directly referenced consts (usually enums)
269
+ @dataclass
270
+ class FpyVariable:
271
+ """a mutable, typed value referenced by an unqualified name"""
272
+
273
+ type_ref: AstExpr
274
+ """the expression denoting the var's type"""
275
+ declaration: AstAssign
276
+ """the node where this var is declared"""
277
+ type: FppTypeClass | None = None
278
+ """the resolved type of the variable. None if type unsure at the moment"""
279
+ lvar_offset: int | None = None
280
+ """the offset in the lvar array where this var is stored"""
281
+
282
+
283
+ # a scope
284
+ FpyScope = dict[str, "FpyReference"]
285
+
286
+
287
+ def create_scope(
288
+ references: dict[str, "FpyReference"],
289
+ ) -> FpyScope:
290
+ """from a flat dict of strs to references, creates a hierarchical, scoped
291
+ dict. no two leaf nodes may have the same name"""
292
+
293
+ base = {}
294
+
295
+ for fqn, ref in references.items():
296
+ names_strs = fqn.split(".")
297
+
298
+ ns = base
299
+ while len(names_strs) > 1:
300
+ existing_child = ns.get(names_strs[0], None)
301
+ if existing_child is None:
302
+ # this scope is not defined atm
303
+ existing_child = {}
304
+ ns[names_strs[0]] = existing_child
305
+
306
+ if not isinstance(existing_child, dict):
307
+ # something else already has this name
308
+ print(
309
+ f"WARNING: {fqn} is already defined as {existing_child}, tried to redefine it as {ref}"
310
+ )
311
+ break
312
+
313
+ ns = existing_child
314
+ names_strs = names_strs[1:]
315
+
316
+ if len(names_strs) != 1:
317
+ # broke early. skip this loop
318
+ continue
319
+
320
+ # okay, now ns is the complete scope of the attribute
321
+ # i.e. everything up until the last '.'
322
+ name = names_strs[0]
323
+
324
+ existing_child = ns.get(name, None)
325
+
326
+ if existing_child is not None:
327
+ # uh oh, something already had this name with a diff value
328
+ print(
329
+ f"WARNING: {fqn} is already defined as {existing_child}, tried to redefine it as {ref}"
330
+ )
331
+ continue
332
+
333
+ ns[name] = ref
334
+
335
+ return base
336
+
337
+
338
+ def union_scope(lhs: FpyScope, rhs: FpyScope) -> FpyScope:
339
+ """returns the two scopes, joined into one. if there is a conflict, chooses lhs over rhs"""
340
+ lhs_keys = set(lhs.keys())
341
+ rhs_keys = set(rhs.keys())
342
+ common_keys = lhs_keys.intersection(rhs_keys)
343
+
344
+ only_lhs_keys = lhs_keys.difference(common_keys)
345
+ only_rhs_keys = rhs_keys.difference(common_keys)
346
+
347
+ new = FpyScope()
348
+
349
+ for key in common_keys:
350
+ if not isinstance(lhs[key], dict) or not isinstance(rhs[key], dict):
351
+ # cannot be merged cleanly. one of the two is not a scope
352
+ print(f"WARNING: {key} is defined as {lhs[key]}, ignoring {rhs[key]}")
353
+ new[key] = lhs[key]
354
+ continue
355
+
356
+ new[key] = union_scope(lhs[key], rhs[key])
357
+
358
+ for key in only_lhs_keys:
359
+ new[key] = lhs[key]
360
+ for key in only_rhs_keys:
361
+ new[key] = rhs[key]
362
+
363
+ return new
364
+
365
+
366
+ FpyReference = typing.Union[
367
+ ChTemplate,
368
+ PrmTemplate,
369
+ FppType,
370
+ FpyCallable,
371
+ FppTypeClass,
372
+ FpyVariable,
373
+ FieldReference,
374
+ dict, # dict of FpyReference
375
+ ]
376
+ """some named concept in fpy"""
377
+
378
+
379
+ def get_ref_fpp_type_class(ref: FpyReference) -> FppTypeClass:
380
+ """returns the fprime type of the ref, if it were to be evaluated as an expression"""
381
+ if isinstance(ref, ChTemplate):
382
+ result_type = ref.ch_type_obj
383
+ elif isinstance(ref, PrmTemplate):
384
+ result_type = ref.prm_type_obj
385
+ elif isinstance(ref, FppType):
386
+ # constant value
387
+ result_type = type(ref)
388
+ elif isinstance(ref, FpyCallable):
389
+ # a reference to a callable isn't a type in and of itself
390
+ # it has a return type but you have to call it (with an AstFuncCall)
391
+ # consider making a separate "reference" type
392
+ result_type = NothingType
393
+ elif isinstance(ref, FpyVariable):
394
+ result_type = ref.type
395
+ elif isinstance(ref, type):
396
+ # a reference to a type doesn't have a value, and so doesn't have a type,
397
+ # in and of itself. if this were a function call to the type's ctor then
398
+ # it would have a value and thus a type
399
+ result_type = NothingType
400
+ elif isinstance(ref, FieldReference):
401
+ result_type = ref.type
402
+ elif isinstance(ref, dict):
403
+ # reference to a scope. scopes don't have values
404
+ result_type = NothingType
405
+ else:
406
+ assert False, ref
407
+
408
+ return result_type
409
+
410
+
411
+ @dataclass
412
+ class CompileState:
413
+ """a collection of input, internal and output state variables and maps"""
414
+
415
+ types: FpyScope
416
+ """a scope whose leaf nodes are subclasses of BaseType"""
417
+ callables: FpyScope
418
+ """a scope whose leaf nodes are FpyCallable instances"""
419
+ tlms: FpyScope
420
+ """a scope whose leaf nodes are ChTemplates"""
421
+ prms: FpyScope
422
+ """a scope whose leaf nodes are PrmTemplates"""
423
+ consts: FpyScope
424
+ """a scope whose leaf nodes are instances of subclasses of BaseType"""
425
+ variables: FpyScope = field(default_factory=dict)
426
+ """a scope whose leaf nodes are FpyVariables"""
427
+ runtime_values: FpyScope = None
428
+ """a scope whose leaf nodes are tlms/prms/consts/variables, all of which
429
+ have some value at runtime."""
430
+
431
+ def __post_init__(self):
432
+ self.runtime_values = union_scope(
433
+ self.tlms,
434
+ union_scope(self.prms, union_scope(self.consts, self.variables)),
435
+ )
436
+
437
+ resolved_references: dict[AstReference, FpyReference] = field(
438
+ default_factory=dict, repr=False
439
+ )
440
+ """reference to its singular resolution"""
441
+
442
+ expr_types: dict[AstExpr, FppTypeClass | NothingTypeClass] = field(
443
+ default_factory=dict
444
+ )
445
+ """expr to its fprime type, or nothing type if none"""
446
+
447
+ stack_op_directives: dict[AstOp, type[StackOpDirective]] = field(
448
+ default_factory=dict
449
+ )
450
+ """some stack operation to which directive will be emitted for it"""
451
+
452
+ type_coercions: dict[AstExpr, FppTypeClass] = field(default_factory=dict)
453
+ """expr to fprime type it must be converted into at runtime"""
454
+
455
+ expr_values: dict[AstExpr, FppType | NothingType | None] = field(
456
+ default_factory=dict
457
+ )
458
+ """expr to its fprime value, or nothing if no value, or None if unsure at compile time"""
459
+
460
+ directives: dict[Ast, list[Directive] | None] = field(default_factory=dict)
461
+ """a list of code generated by each node, or None/empty list if no directives"""
462
+
463
+ node_dir_counts: dict[Ast, int] = field(default_factory=dict)
464
+ """node to the number of directives generated by it"""
465
+
466
+ lvar_array_size_bytes: int = 0
467
+ """the size in bytes of the lvar array"""
468
+
469
+ start_line_idx: dict[Ast, int] = field(default_factory=dict)
470
+ """the line index at which each node's directives will be included in the output"""
471
+
472
+ errors: list[CompileException] = field(default_factory=list)
473
+ """a list of all compile exceptions generated by passes"""
474
+
475
+ def err(self, msg, n):
476
+ """adds a compile exception to internal state"""
477
+ self.errors.append(CompileException(msg, n))
478
+
479
+
480
+ class Visitor:
481
+ """visits each class, calling a custom visit function, if one is defined, for each
482
+ node type"""
483
+
484
+ def _find_custom_visit_func(self, node: Ast):
485
+ for name, func in inspect.getmembers(type(self), inspect.isfunction):
486
+ if not name.startswith("visit") or name == "visit_default":
487
+ # not a visitor, or the default visit func
488
+ continue
489
+ signature = inspect.signature(func)
490
+ params = list(signature.parameters.values())
491
+ assert len(params) == 3
492
+ assert params[1].annotation is not None
493
+ annotations = typing.get_type_hints(func)
494
+ param_type = annotations[params[1].name]
495
+ if is_instance_compat(node, param_type):
496
+ return func
497
+ else:
498
+ # call the default
499
+ return type(self).visit_default
500
+
501
+ def _visit(self, node: Ast, state: CompileState):
502
+ visit_func = self._find_custom_visit_func(node)
503
+ visit_func(self, node, state)
504
+
505
+ def visit_default(self, node: Ast, state: CompileState):
506
+ pass
507
+
508
+ def run(self, start: Ast, state: CompileState):
509
+ """runs the visitor, starting at the given node, descending depth-first"""
510
+
511
+ def _descend(node: Ast):
512
+ if not isinstance(node, Ast):
513
+ return
514
+ children = []
515
+ for field in fields(node):
516
+ field_val = getattr(node, field.name)
517
+ if isinstance(field_val, list):
518
+ children.extend(field_val)
519
+ else:
520
+ children.append(field_val)
521
+
522
+ for child in children:
523
+ if not isinstance(child, Ast):
524
+ continue
525
+ _descend(child)
526
+ if len(state.errors) != 0:
527
+ break
528
+ self._visit(child, state)
529
+ if len(state.errors) != 0:
530
+ break
531
+
532
+ _descend(start)
533
+ self._visit(start, state)
534
+
535
+
536
+ class TopDownVisitor(Visitor):
537
+
538
+ def run(self, start: Ast, state: CompileState):
539
+ """runs the visitor, starting at the given node, descending breadth-first"""
540
+
541
+ def _descend(node: Ast):
542
+ if not isinstance(node, Ast):
543
+ return
544
+ children = []
545
+ for field in fields(node):
546
+ field_val = getattr(node, field.name)
547
+ if isinstance(field_val, list):
548
+ children.extend(field_val)
549
+ else:
550
+ children.append(field_val)
551
+
552
+ for child in children:
553
+ if not isinstance(child, Ast):
554
+ continue
555
+ self._visit(child, state)
556
+ if len(state.errors) != 0:
557
+ break
558
+ _descend(child)
559
+ if len(state.errors) != 0:
560
+ break
561
+
562
+ self._visit(start, state)
563
+ _descend(start)
@@ -318,6 +318,7 @@ class ConfigDrivenParser(ParserBase):
318
318
 
319
319
  DEFAULT_CONFIGURATION_PATH = Path("fprime-gds.yml")
320
320
 
321
+
321
322
  @classmethod
322
323
  def set_default_configuration(cls, path: Path):
323
324
  """Set path for (global) default configuration file
@@ -413,6 +414,14 @@ class ConfigDrivenParser(ParserBase):
413
414
  print(f"[INFO] Reading command-line configuration from: {args.config}")
414
415
  with open(args.config, "r") as file_handle:
415
416
  try:
417
+ relative_base = args.config.parent.absolute()
418
+
419
+ def path_constructor(loader, node):
420
+ """ Processes !PATH annotations as relative to current file """
421
+ calculated_path = relative_base / loader.construct_scalar(node)
422
+ return calculated_path
423
+
424
+ yaml.SafeLoader.add_constructor("!PATH", path_constructor)
416
425
  loaded = yaml.safe_load(file_handle)
417
426
  args.config_values = loaded if loaded is not None else {}
418
427
  except Exception as exc:
@@ -11,6 +11,7 @@ import webbrowser
11
11
  from fprime_gds.executables.cli import (
12
12
  BinaryDeployment,
13
13
  ConfigDrivenParser,
14
+ CompositeParser,
14
15
  CommParser,
15
16
  GdsParser,
16
17
  ParserBase,
@@ -104,6 +105,7 @@ def launch_html(parsed_args):
104
105
  Return:
105
106
  launched process
106
107
  """
108
+ composite_parser = CompositeParser([StandardPipelineParser, ConfigDrivenParser])
107
109
  reproduced_arguments = StandardPipelineParser().reproduce_cli_args(parsed_args)
108
110
  if "--log-directly" not in reproduced_arguments:
109
111
  reproduced_arguments += ["--log-directly"]
fprime_gds/flask/app.py CHANGED
@@ -29,7 +29,7 @@ import fprime_gds.flask.logs
29
29
  import fprime_gds.flask.sequence
30
30
  import fprime_gds.flask.stats
31
31
  import fprime_gds.flask.updown
32
- from fprime_gds.executables.cli import ParserBase, StandardPipelineParser
32
+ from fprime_gds.executables.cli import ParserBase, StandardPipelineParser, ConfigDrivenParser
33
33
 
34
34
  from . import components
35
35
 
@@ -59,6 +59,7 @@ def construct_app():
59
59
  compress.init_app(app)
60
60
 
61
61
  app.config.from_object("fprime_gds.flask.default_settings")
62
+
62
63
  # Override defaults from python files specified in 'FP_FLASK_SETTINGS'
63
64
  if "FP_FLASK_SETTINGS" in os.environ:
64
65
  app.config.from_envvar("FP_FLASK_SETTINGS")
@@ -69,10 +70,15 @@ def construct_app():
69
70
  # Standard pipeline creation
70
71
  input_arguments = app.config["STANDARD_PIPELINE_ARGUMENTS"]
71
72
  args_ns, _ = ParserBase.parse_args(
72
- [StandardPipelineParser], "n/a", input_arguments, client=True
73
+ [StandardPipelineParser, ConfigDrivenParser], "n/a", input_arguments, client=True
73
74
  )
75
+ # Load app configuration from file
76
+ for key, value in args_ns.config_values.get("flask", {}).items():
77
+ app.config[key] = value
78
+
74
79
  pipeline = components.setup_pipelined_components(app.debug, args_ns)
75
80
 
81
+
76
82
  # Restful API registration
77
83
  api = fprime_gds.flask.errors.setup_error_handling(app)
78
84
 
@@ -194,6 +200,15 @@ def handle_unexpected_error(error):
194
200
  return flask.jsonify(response), status_code
195
201
 
196
202
 
203
+ @app.route("/js/config.js")
204
+ def config_serve():
205
+ """
206
+ Serve the config.js file
207
+
208
+ :param path: path to the file (in terms of web browser)
209
+ """
210
+ return flask.send_file(app.config["JS_CONFIGURATION_FILE"])
211
+
197
212
  @app.route("/js/<path:path>")
198
213
  def files_serve(path):
199
214
  """
@@ -16,5 +16,6 @@ SERVE_LOGS = os.environ.get("SERVE_LOGS", "YES") == "YES"
16
16
 
17
17
  MAX_CONTENT_LENGTH = 32 * 1024 * 1024 # Max length of request is 32MiB
18
18
 
19
+ JS_CONFIGURATION_FILE = os.path.join(os.path.dirname(__file__), "static", "js", "config.js")
19
20
 
20
21
  # TODO: load real config