Skip to content

Commit

Permalink
JSON Dictionary Integration (#173)
Browse files Browse the repository at this point in the history
* loading channels working - minus glutton of choice

* Handle struct member arrays (inline)

* Add JsonLoader base type

* working MVP for all cmd/channels/events

* Remove commented out code

* Update for latest JSON format

* Formatting

* Fix Python<3.10 compatibility (remove match statement)

* Translate format strings at load-time

* Remove tuple type hint that break Python3.8

* Fix CodeQL warnings in test code

* spelling

* Fix changed test case for more surface area

* Formatting

* Fix get_versions()

* reorganize code

* Improve behavior of default command selection

* Prioritize selection of JSON dictionary over XML

* Cache parsed_types in JsonLoader class

* Add test cases

* ignore some spelling

* Code cleanup and formatting

* Remove "tag" terminology

* Exclude dictionary from spelling

* few review notes

* case-insensitive sorting in commandList

* Improve error handling

* Sort dictionaries at load time
  • Loading branch information
thomas-bc authored May 22, 2024
1 parent eecb6f5 commit db2ef3c
Show file tree
Hide file tree
Showing 19 changed files with 10,621 additions and 132 deletions.
7 changes: 4 additions & 3 deletions .github/actions/spelling/excludes.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
pyproject.toml
setup.py
ignore$
/html/
(?:^|/)(?i)COPYRIGHT
(?:^|/)(?i)LICEN[CS]E
(?:^|/)vendor/
^\.github/
^Autocoders/Python/test/.*\.xml$
RefTopologyDictionary.json
dictionary.xml
/doc/xml/
/third-party/
/third/
Expand Down Expand Up @@ -43,6 +47,3 @@ ignore$
\.xlsx$
\.xsd$
\.ico$
pyproject.toml
setup.py
dictionary.xml
3 changes: 3 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ CCB
CCFF
cdn
Cdioux
cdxoefg
CFDG
CFDP
CFPD
Expand Down Expand Up @@ -229,6 +230,7 @@ focusring
FONTNAME
FONTPATH
FONTSIZE
fpp
fprime
fptable
frac
Expand Down Expand Up @@ -420,6 +422,7 @@ nitpicky
noapp
noqa
normpath
NOSONAR
nosort
Noto
novalidate
Expand Down
7 changes: 6 additions & 1 deletion src/fprime_gds/common/data_types/event_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ def __init__(self, event_args, event_time, event_temp):
self.display_text = event_temp.description
elif event_temp.format_str == "":
args_template = self.template.get_args()
self.display_text = str([{args_template[index][0]:arg.val} for index, arg in enumerate(event_args)])
self.display_text = str(
[
{args_template[index][0]: arg.val}
for index, arg in enumerate(event_args)
]
)
else:
self.display_text = format_string_template(
event_temp.format_str, tuple([arg.val for arg in event_args])
Expand Down
27 changes: 16 additions & 11 deletions src/fprime_gds/common/data_types/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@
Created on Jan 9, 2015
@author: reder
"""

# Exception classes for all controllers modules


class GseControllerException(Exception):
class FprimeGdsException(Exception):
"""
Base Exception for exceptions that need to be handled at the system-level
by the GDS
"""

pass


class GdsDictionaryParsingException(FprimeGdsException):
pass


# Note: Gse is the historical name for what is now called the GDS
class GseControllerException(FprimeGdsException):
def __init__(self, val):
self.except_msg = val
super().__init__(val)
Expand All @@ -27,13 +42,3 @@ def __init__(self, val):
class GseControllerParsingException(GseControllerException):
def __init__(self, val):
super().__init__(f"Parsing error: {str(val)}")


class GseControllerMnemonicMismatchException(GseControllerException):
def __init__(self, val1, val2):
super().__init__(f"ID mismatch ({str(val1)}, {str(val2)})!")


class GseControllerStatusUpdateException(GseControllerException):
def __init__(self, val):
super().__init__(f"Bad status update mode: {str(val)}!")
107 changes: 107 additions & 0 deletions src/fprime_gds/common/loaders/ch_json_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
ch_json_loader.py:
Loads flight dictionary (JSON) and returns id and mnemonic based Python dictionaries of channels
@author thomas-bc
"""

from fprime_gds.common.templates.ch_template import ChTemplate
from fprime_gds.common.loaders.json_loader import JsonLoader
from fprime_gds.common.data_types.exceptions import GdsDictionaryParsingException


class ChJsonLoader(JsonLoader):
"""Class to load python based telemetry channel dictionaries"""

CHANNELS_FIELD = "telemetryChannels"

ID = "id"
NAME = "name"
DESC = "annotation"
TYPE = "type"
FMT_STR = "format"
LIMIT_FIELD = "limit"
LIMIT_LOW = "low"
LIMIT_HIGH = "high"
LIMIT_RED = "red"
LIMIT_ORANGE = "orange"
LIMIT_YELLOW = "yellow"

def construct_dicts(self, _):
"""
Constructs and returns python dictionaries keyed on id and name
Args:
_: Unused argument (inherited)
Returns:
A tuple with two channel dictionaries (python type dict):
(id_dict, name_dict). The keys should be the channels' id and
name fields respectively and the values should be ChTemplate
objects.
"""
id_dict = {}
name_dict = {}

if self.CHANNELS_FIELD not in self.json_dict:
raise GdsDictionaryParsingException(
f"Ground Dictionary missing '{self.CHANNELS_FIELD}' field: {str(self.json_file)}"
)

for ch_dict in self.json_dict[self.CHANNELS_FIELD]:
# Create a channel template object
ch_temp = self.construct_template_from_dict(ch_dict)

id_dict[ch_temp.get_id()] = ch_temp
name_dict[ch_temp.get_full_name()] = ch_temp

return (
dict(sorted(id_dict.items())),
dict(sorted(name_dict.items())),
self.get_versions(),
)

def construct_template_from_dict(self, channel_dict: dict) -> ChTemplate:
try:
ch_id = channel_dict[self.ID]
# The below assignment also raises a ValueError if the name does not contain a '.'
component_name, channel_name = channel_dict[self.NAME].split(".")
if not component_name or not channel_name:
raise ValueError()

type_obj = self.parse_type(channel_dict[self.TYPE])
except ValueError as e:
raise GdsDictionaryParsingException(
f"Channel dictionary entry malformed, expected name of the form '<COMP_NAME>.<CH_NAME>' in : {str(channel_dict)}"
)
except KeyError as e:
raise GdsDictionaryParsingException(
f"{str(e)} key missing from Channel dictionary entry or its associated type in the dictionary: {str(channel_dict)}"
)

format_str = JsonLoader.preprocess_format_str(channel_dict.get(self.FMT_STR))

limit_field = channel_dict.get(self.LIMIT_FIELD)
limit_low = limit_field.get(self.LIMIT_LOW) if limit_field else None
limit_high = limit_field.get(self.LIMIT_HIGH) if limit_field else None
limit_low_yellow = limit_low.get(self.LIMIT_YELLOW) if limit_low else None
limit_low_red = limit_low.get(self.LIMIT_RED) if limit_low else None
limit_low_orange = limit_low.get(self.LIMIT_ORANGE) if limit_low else None
limit_high_yellow = limit_high.get(self.LIMIT_YELLOW) if limit_high else None
limit_high_red = limit_high.get(self.LIMIT_RED) if limit_high else None
limit_high_orange = limit_high.get(self.LIMIT_ORANGE) if limit_high else None

return ChTemplate(
ch_id,
channel_name,
component_name,
type_obj,
ch_fmt_str=format_str,
ch_desc=channel_dict.get(self.DESC),
low_red=limit_low_red,
low_orange=limit_low_orange,
low_yellow=limit_low_yellow,
high_yellow=limit_high_yellow,
high_orange=limit_high_orange,
high_red=limit_high_red,
)
10 changes: 5 additions & 5 deletions src/fprime_gds/common/loaders/ch_xml_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ def construct_dicts(self, path):
respectively and the values are ChTemplate objects
"""
xml_tree = self.get_xml_tree(path)
versions = xml_tree.attrib.get("framework_version", "unknown"), xml_tree.attrib.get("project_version", "unknown")
versions = xml_tree.attrib.get(
"framework_version", "unknown"
), xml_tree.attrib.get("project_version", "unknown")

# Check if xml dict has channels section
ch_section = self.get_xml_section(self.CH_SECT, xml_tree)
if ch_section is None:
msg = f"Xml dict did not have a {self.CH_SECT} section"
raise exceptions.GseControllerParsingException(
msg
)
raise exceptions.GseControllerParsingException(msg)

id_dict = {}
name_dict = {}
Expand All @@ -84,7 +84,7 @@ def construct_dicts(self, path):
ch_desc = ch_dict[self.DESC_TAG]

if self.FMT_STR_TAG in ch_dict:
ch_fmt_str = ch_dict[self.FMT_STR_TAG]
ch_fmt_str = XmlLoader.preprocess_format_str(ch_dict[self.FMT_STR_TAG])

# TODO we need to convert these into numbers, is this the best
# way to do it?
Expand Down
85 changes: 85 additions & 0 deletions src/fprime_gds/common/loaders/cmd_json_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
cmd_json_loader.py:
Loads flight dictionary (JSON) and returns id and mnemonic based Python dictionaries of commands
@author thomas-bc
"""

from fprime_gds.common.templates.cmd_template import CmdTemplate
from fprime_gds.common.loaders.json_loader import JsonLoader
from fprime_gds.common.data_types.exceptions import GdsDictionaryParsingException


class CmdJsonLoader(JsonLoader):
"""Class to load xml based command dictionaries"""

COMMANDS_FIELD = "commands"

NAME = "name"
OPCODE = "opcode"
DESC = "annotation"
PARAMETERS = "formalParams"

def construct_dicts(self, _):
"""
Constructs and returns python dictionaries keyed on id and name
Args:
_: Unused argument (inherited)
Returns:
A tuple with two command dictionaries (python type dict):
(id_dict, name_dict). The keys are the events' id and name fields
respectively and the values are CmdTemplate objects
"""
id_dict = {}
name_dict = {}

if self.COMMANDS_FIELD not in self.json_dict:
raise GdsDictionaryParsingException(
f"Ground Dictionary missing '{self.COMMANDS_FIELD}' field: {str(self.json_file)}"
)

for cmd_dict in self.json_dict[self.COMMANDS_FIELD]:
cmd_temp = self.construct_template_from_dict(cmd_dict)

id_dict[cmd_temp.get_id()] = cmd_temp
name_dict[cmd_temp.get_full_name()] = cmd_temp

return (
dict(sorted(id_dict.items())),
dict(sorted(name_dict.items())),
self.get_versions(),
)

def construct_template_from_dict(self, cmd_dict: dict) -> CmdTemplate:
try:
cmd_comp, cmd_mnemonic = cmd_dict[self.NAME].split(".")
cmd_opcode = cmd_dict[self.OPCODE]
cmd_desc = cmd_dict.get(self.DESC)
except ValueError as e:
raise GdsDictionaryParsingException(
f"Command dictionary entry malformed, expected name of the form '<COMP_NAME>.<CMD_NAME>' in : {str(cmd_dict)}"
)
except KeyError as e:
raise GdsDictionaryParsingException(
f"{str(e)} key missing from Command dictionary entry: {str(cmd_dict)}"
)
# Parse Arguments
cmd_args = []
for param in cmd_dict.get(self.PARAMETERS, []):
try:
param_name = param["name"]
param_type = self.parse_type(param["type"])
except KeyError as e:
raise GdsDictionaryParsingException(
f"{str(e)} key missing from Command parameter or its associated type in the dictionary: {str(param)}"
)
cmd_args.append(
(
param_name,
param.get("annotation"),
param_type,
)
)
return CmdTemplate(cmd_opcode, cmd_mnemonic, cmd_comp, cmd_args, cmd_desc)
2 changes: 1 addition & 1 deletion src/fprime_gds/common/loaders/dict_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def get_name_dict(self, path):
return name_dict

def get_versions(self):
""" Get version tuple """
"""Get version tuple"""
return self.versions

def construct_dicts(self, path):
Expand Down
Loading

0 comments on commit db2ef3c

Please sign in to comment.