Skip to content

Commit

Permalink
add xtce gen script
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanpdx committed Nov 27, 2023
1 parent 3f70437 commit 9845af4
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,6 @@ OD.*

# generated docs
**/gen/*.rst

# Beacon def formats
*xtce.xml
4 changes: 4 additions & 0 deletions oresat_configs/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
from .constants import __version__
from .scripts.gen_dcf import GEN_DCF, GEN_DCF_PROG, gen_dcf
from .scripts.gen_fw_files import GEN_FW_FILES, GEN_FW_FILES_PROG, gen_fw_files
from .scripts.gen_xtce import GEN_XTCE, GEN_XTCE_PROG, gen_xtce
from .scripts.print_od import PRINT_OD, PRINT_OD_PROG, print_od
from .scripts.sdo_transfer import SDO_TRANSFER, SDO_TRANSFER_PROG, sdo_transfer

SCRIPTS = {
GEN_DCF_PROG: GEN_DCF,
GEN_XTCE_PROG: GEN_XTCE,
GEN_FW_FILES_PROG: GEN_FW_FILES,
PRINT_OD_PROG: PRINT_OD,
SDO_TRANSFER_PROG: SDO_TRANSFER,
Expand Down Expand Up @@ -39,5 +41,7 @@ def oresat_configs():
print_od(sys.argv[2:])
elif sys.argv[1] == SDO_TRANSFER_PROG:
sdo_transfer(sys.argv[2:])
elif sys.argv[1] == GEN_XTCE_PROG:
gen_xtce(sys.argv[2:])
else:
oresat_configs()
2 changes: 1 addition & 1 deletion oresat_configs/_yaml_to_od.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ def _gen_od_db(oresat_id: OreSatId, beacon_def: BeaconConfig, configs: dict) ->
od["satellite_id"].default = oresat_id.value
if node_id == NodeId.C3:
for oid in list(OreSatId):
od["satellite_id"].value_descriptions[oid.value] = oid.name
od["satellite_id"].value_descriptions[oid.value] = oid.name.lower()
od["beacon"]["revision"].default = beacon_def.revision
od["beacon"]["dest_callsign"].default = beacon_def.ax25.dest_callsign
od["beacon"]["dest_ssid"].default = beacon_def.ax25.dest_ssid
Expand Down
303 changes: 303 additions & 0 deletions oresat_configs/scripts/gen_xtce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
"""Generate XTCE for the beacon."""

import sys
import xml.etree.ElementTree as ET
from argparse import ArgumentParser
from datetime import datetime

import canopen

from .. import ORESAT_NICE_NAMES, NodeId, OreSatConfig, OreSatId

GEN_XTCE = "generate beacon xtce file"
GEN_XTCE_PROG = "oresat-gen-xtce"

CANOPEN_TO_XTCE_DT = {
canopen.objectdictionary.BOOLEAN: "bool",
canopen.objectdictionary.INTEGER8: "int8",
canopen.objectdictionary.INTEGER16: "int16",
canopen.objectdictionary.INTEGER32: "int32",
canopen.objectdictionary.INTEGER64: "int64",
canopen.objectdictionary.UNSIGNED8: "uint8",
canopen.objectdictionary.UNSIGNED16: "uint16",
canopen.objectdictionary.UNSIGNED32: "uint32",
canopen.objectdictionary.UNSIGNED64: "uint64",
canopen.objectdictionary.VISIBLE_STRING: "string",
canopen.objectdictionary.REAL32: "float",
canopen.objectdictionary.REAL64: "double",
}

DT_LEN = {
canopen.objectdictionary.BOOLEAN: 8,
canopen.objectdictionary.INTEGER8: 8,
canopen.objectdictionary.INTEGER16: 16,
canopen.objectdictionary.INTEGER32: 32,
canopen.objectdictionary.INTEGER64: 64,
canopen.objectdictionary.UNSIGNED8: 8,
canopen.objectdictionary.UNSIGNED16: 16,
canopen.objectdictionary.UNSIGNED32: 32,
canopen.objectdictionary.UNSIGNED64: 64,
canopen.objectdictionary.VISIBLE_STRING: 0,
canopen.objectdictionary.REAL32: 32,
canopen.objectdictionary.REAL64: 64,
}


def make_obj_name(obj: canopen.objectdictionary.Variable) -> str:
"""get obj name."""

name = ""
if obj.index < 0x5000:
name += "c3_"

if isinstance(obj.parent, canopen.ObjectDictionary):
name += obj.name
else:
name += f"{obj.parent.name}_{obj.name}"

return name


def make_dt_name(obj) -> str:
"""Make xtce data type name."""

type_name = CANOPEN_TO_XTCE_DT[obj.data_type]
if obj.name in ["unix_time", "updater_status"]:
type_name = obj.name
elif obj.value_descriptions:
if isinstance(obj.parent, canopen.ObjectDictionary):
type_name += f"_c3_{obj.name}"
else:
type_name += f"_{obj.parent.name}_{obj.name}"
elif obj.data_type == canopen.objectdictionary.VISIBLE_STRING:
type_name += f"{len(obj.default) * 8}"
elif obj.unit:
type_name += f"_{obj.unit}"
type_name = type_name.replace("/", "p").replace("%", "percent")

type_name += "_type"

return type_name


def write_xtce(config: OreSatConfig, dir_path: str = "."):
"""Write beacon configs to a xtce file."""

root = ET.Element(
"SpaceSystem",
attrib={
"name": ORESAT_NICE_NAMES[config.oresat_id],
"xmlns:xtce": "http://www.omg.org/space/xtce",
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation": (
"http://www.omg.org/spec/XTCE/20180204 "
"https://www.omg.org/spec/XTCE/20180204/SpaceSystem.xsd"
),
},
)

header = ET.SubElement(
root,
"Header",
attrib={
"validationStatus": "Working",
"classification": "NotClassified",
"version": f'{config.od_db[NodeId.C3]["beacon"]["revision"].value}.0',
"date": datetime.now().strftime("%Y-%m-%d"),
},
)
author_set = ET.SubElement(header, "AuthorSet")
author = ET.SubElement(author_set, "Author")
author.text = "PSAS (Portland State Aerospace Society)"

tm_meta = ET.SubElement(root, "TelemetryMetaData")
tm_meta_para = ET.SubElement(tm_meta, "ParameterTypeSet")

para_type = ET.SubElement(
tm_meta_para,
"AbsoluteTimeParameterType",
attrib={
"name": "unix_time",
"shortDescription": "Unix coarse timestamp",
},
)
enc = ET.SubElement(para_type, "Encodings")
ET.SubElement(
enc,
"IntegerDataEncoding",
attrib={
"byteOrder": "leastSignificantByteFirst",
"sizeInBits": "32",
},
)
ref_time = ET.SubElement(para_type, "ReferenceTime")
epoch = ET.SubElement(ref_time, "Epoch")
epoch.text = "1970-01-01T00:00.00.000"

para_types = ["unix_time"]
for obj in config.beacon_def:
name = make_dt_name(obj)
if name in para_types:
continue
para_types.append(name)

if obj.data_type == canopen.objectdictionary.BOOLEAN:
para_type = ET.SubElement(
tm_meta_para,
"BooleanParameterType",
attrib={
"name": name,
"zeroStringValue": "0",
"oneStringValue": "1",
},
)
elif obj.data_type in canopen.objectdictionary.UNSIGNED_TYPES and obj.value_descriptions:
para_type = ET.SubElement(
tm_meta_para,
"EnumeratedParameterType",
attrib={
"name": name,
},
)
enum_list = ET.SubElement(para_type, "EnumerationList")
for value, name in obj.value_descriptions.items():
ET.SubElement(
enum_list,
"Enumeration",
attrib={
"value": str(value),
"label": name,
},
)
elif obj.data_type in canopen.objectdictionary.INTEGER_TYPES:
if obj.data_type in canopen.objectdictionary.UNSIGNED_TYPES:
signed = False
encoding = "unsigned"
else:
signed = True
encoding = "twoComplement"

para_type = ET.SubElement(
tm_meta_para,
"IntegerParameterType",
attrib={
"name": name,
"signed": str(signed).lower(),
},
)

para_unit_set = ET.SubElement(para_type, "UnitSet")
if obj.unit:
para_unit = ET.SubElement(
para_unit_set,
"Unit",
attrib={
"description": obj.unit,
},
)
para_unit.text = obj.unit

data_enc = ET.SubElement(
para_type,
"IntegerDataEncoding",
attrib={
"byteOrder": "leastSignificantByteFirst",
"encoding": encoding,
"sizeInBits": str(DT_LEN[obj.data_type]),
},
)
if obj.factor != 1:
def_cal = ET.SubElement(data_enc, "DefaultCalibrator")
poly_cal = ET.SubElement(def_cal, "PolynomialCalibrator")
ET.SubElement(
poly_cal,
"Term",
attrib={
"exponent": "1",
"coefficient": str(obj.factor),
},
)
elif obj.data_type == canopen.objectdictionary.VISIBLE_STRING:
para_type = ET.SubElement(
tm_meta_para,
"StringParameterType",
attrib={
"name": name,
"baseType": CANOPEN_TO_XTCE_DT[obj.data_type],
},
)
str_para_type = ET.SubElement(
para_type,
"StringParameterType",
attrib={
"encoding": "UTF-8",
},
)
size_in_bits = ET.SubElement(str_para_type, "SizeInBits")
fixed = ET.SubElement(size_in_bits, "Fixed")
fixed_value = ET.SubElement(fixed, "FixedValue")
fixed_value.text = str(len(obj.default) * 8)

para_set = ET.SubElement(tm_meta, "ParameterSet")
for obj in config.beacon_def:
ET.SubElement(
para_set,
"Parameter",
attrib={
"name": make_obj_name(obj),
"parameterTypeRef": make_dt_name(obj),
"shortDescription": obj.description,
},
)

cont_set = ET.SubElement(tm_meta, "ContainerSet")
seq_cont = ET.SubElement(
cont_set,
"SequenceContainer",
attrib={
"name": "Beacon",
},
)
entry_list = ET.SubElement(seq_cont, "EntryList")
for obj in config.beacon_def:
ET.SubElement(
entry_list,
"ParameterRefEntry",
attrib={
"parameterRef": make_obj_name(obj),
},
)

# write
tree = ET.ElementTree(root)
ET.indent(tree, space=" ", level=0)
file_name = f"{config.oresat_id.name.lower()}-xtce.xml"
tree.write(f"{dir_path}/{file_name}", encoding="utf-8", xml_declaration=True)


def gen_xtce(sys_args=None):
"""Gen_dcf main."""

if sys_args is None:
sys_args = sys.argv[1:]

parser = ArgumentParser(description=GEN_XTCE, prog=GEN_XTCE_PROG)
parser.add_argument(
"oresat", default="oresat0", help="oresat mission; oresat0, oresat0.5, or oresat1"
)
parser.add_argument("-d", "--dir-path", default=".", help='directory path; defautl "."')
args = parser.parse_args(sys_args)

arg_oresat = args.oresat.lower()
if arg_oresat in ["0", "oresat0"]:
oresat_id = OreSatId.ORESAT0
elif arg_oresat in ["0.5", "oresat0.5"]:
oresat_id = OreSatId.ORESAT0_5
elif arg_oresat in ["1", "oresat1"]:
oresat_id = OreSatId.ORESAT1
else:
print(f"invalid oresat mission: {args.oresat}")
sys.exit()

config = OreSatConfig(oresat_id)
write_xtce(config, args.dir_path)

0 comments on commit 9845af4

Please sign in to comment.