mirror of
https://github.com/nasa/fprime.git
synced 2025-12-10 17:47:10 -06:00
349 lines
13 KiB
Python
Executable File
349 lines
13 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#===============================================================================
|
|
# NAME: tinyseqgen
|
|
#
|
|
# DESCRIPTION: A tiny sequence generator for F Prime. This sequence compiler takes a
|
|
# .seq file as input and produces a binary sequence file compatible with the
|
|
# F Prime sequence file loader and sequence file runner.
|
|
# AUTHOR: Kevin Dinkel
|
|
# EMAIL: dinkel@jpl.nasa.gov
|
|
# DATE CREATED: December 15, 2015
|
|
#
|
|
# Copyright 2015, California Institute of Technology.
|
|
# ALL RIGHTS RESERVED. U.S. Government Sponsorship acknowledged.
|
|
#===============================================================================
|
|
|
|
import sys
|
|
import re
|
|
import os
|
|
import copy
|
|
from datetime import datetime, timedelta
|
|
|
|
from fprime.gse.models.serialize.type_exceptions import *
|
|
|
|
__author__ = "Kevin Dinkel"
|
|
__copyright__ = "Copyright 2015, California Institute of Technology."
|
|
__version__ = "1.0"
|
|
__email__ = "kevin.dinkel@jpl.nasa.gov"
|
|
|
|
def __error(string):
|
|
'''
|
|
Print an error message and exit with error code 1
|
|
@param string: the custom error string to print
|
|
'''
|
|
print string
|
|
sys.exit(1)
|
|
|
|
# try:
|
|
from fprime.gse.models.common.command import Descriptor
|
|
from fprime.gse.views.seq_panel import SeqBinaryWriter
|
|
from fprime.gse.controllers import command_loader
|
|
from fprime.gse.controllers import exceptions as gseExceptions
|
|
|
|
# except:
|
|
# __error("The Gse source code was not found in your $PYTHONPATH variable. Please set PYTHONPATH to something like: $BUILD_ROOT/Gse/src:$BUILD_ROOT/Gse/generated/$DEPLOYMENT_NAME")
|
|
|
|
def __errorLine(lineNumber, string):
|
|
'''
|
|
Print an error message relating to a line number of the file input and exit with error code 1
|
|
@param lineNumber: the current line number being parsed
|
|
@param string: the custom error string to print
|
|
'''
|
|
__error("Error on line %d: %s" % (lineNumber + 1, string))
|
|
|
|
def __parse(seqfile):
|
|
'''
|
|
Generator that parses an input sequence file and returns a tuple
|
|
for each valid line of the sequence file.
|
|
@param seqfile: A sequence file name (usually a .seq extension)
|
|
@return A list of tuples:
|
|
(lineNumber, descriptor, seconds, useconds, mnemonic, arguments)
|
|
'''
|
|
|
|
def subQuoted(f, string):
|
|
'''
|
|
Run a substitution function on only substrings within a string that are surrounded
|
|
by single or double quotes
|
|
@param f: a string substitution acting on matchobjs
|
|
@param string: the string to perform the substitution on
|
|
@return the substituted string
|
|
'''
|
|
s = re.sub(r'"[^"]*"', f, string)
|
|
return re.sub(r"'[^']*'", f, s)
|
|
|
|
def removeTrailingComments(string):
|
|
'''
|
|
Remove any trailing comments (proceded by ';') in a string
|
|
@param string: the string to perform comment removal on
|
|
@return the string without trailing comments
|
|
'''
|
|
def replaceSemis(matchobj):
|
|
return matchobj.group(0).replace(';', ' ')
|
|
# ignore all semicolons in quotes:
|
|
s = subQuoted(replaceSemis, string)
|
|
s = subQuoted(replaceSemis, s)
|
|
# get index of first semicolon, and return everything before it:
|
|
if ';' in s:
|
|
index = s.index(';')
|
|
return string[:index]
|
|
# return original string if no semicolon found:
|
|
return string
|
|
|
|
def splitString(string):
|
|
'''
|
|
Split a string with ' ' or ',' as a delimiter. Ignore any delimiters
|
|
found within quoted substrings.
|
|
@param string: the string to perform the split on
|
|
@return a list representing the split string
|
|
'''
|
|
def replaceSpacesAndCommas(matchobj):
|
|
s = re.sub('\s', '_', matchobj.group(0))
|
|
s = re.sub('\,', '_', s)
|
|
return s
|
|
# ignore all spaces in quotes:
|
|
s = subQuoted(replaceSpacesAndCommas, string)
|
|
# replace all commas with spaces, since either can be a delimiter:
|
|
s = s.replace(',', ' ')
|
|
# get the split indices of the modified string:
|
|
indices = [(m.start(), m.end()) for m in re.finditer(r'\S+', s)]
|
|
toReturn = []
|
|
for start, end in indices:
|
|
toReturn.append(string[start:end])
|
|
return toReturn
|
|
|
|
def parseArgs(args):
|
|
'''
|
|
Turn .seq command argument list into their appropriate python types
|
|
@param args: a list of parsed arguments
|
|
@return a list of arguments as native python types
|
|
'''
|
|
def parseArg(arg):
|
|
# See if argument is a string, if so remove the quotes and return it:
|
|
if (arg[0] == '"' and arg[-1] == '"') or (arg[0] == "'" and arg[-1] == "'"):
|
|
return arg[1:-1]
|
|
# If the string contains a "." assume that it is a float:
|
|
elif "." in arg:
|
|
return float(arg)
|
|
elif arg == 'True' or arg == 'true' or arg == 'TRUE':
|
|
return True
|
|
elif arg == 'False' or arg == 'false' or arg == 'FALSE':
|
|
return False
|
|
else:
|
|
try:
|
|
# See if it translates to an integer:
|
|
return int(arg,0)
|
|
except ValueError:
|
|
try:
|
|
# See if it translates to a float:
|
|
return float(arg)
|
|
except ValueError:
|
|
# Otherwise it is an enum type:
|
|
return str(arg)
|
|
return map(parseArg, args)
|
|
|
|
def parseTime(lineNumber, time):
|
|
'''
|
|
Parse a time string and return the command descriptor, seconds, and useconds of the time string
|
|
@param lineNumber: the current line number where the time string was parsed
|
|
@param time: the time string to parse
|
|
@return a tuple (descriptor, seconds, useconds)
|
|
'''
|
|
|
|
def parseTimeStringOption(timeStr, timeFmts):
|
|
'''
|
|
Parse a time string by trying to use different time formats, until one succeeds
|
|
@param timeStr: the time string
|
|
@param timeFmts: the time format used to parse the string
|
|
@return the datetime object containing the parsed string
|
|
'''
|
|
def parseTimeString(timeFmt):
|
|
try:
|
|
return datetime.strptime(timeStr, timeFmt)
|
|
except:
|
|
return None
|
|
|
|
for fmt in timeFmts:
|
|
dt = parseTimeString(fmt)
|
|
if dt:
|
|
return dt
|
|
raise BaseException
|
|
|
|
def parseRelative(timeStr):
|
|
'''
|
|
Parse a relative time string
|
|
@param timeStr: the time string
|
|
@return the datetime object containing the parsed string
|
|
'''
|
|
options = ["%H:%M:%S.%f", "%H:%M:%S"]
|
|
return parseTimeStringOption(timeStr, options)
|
|
|
|
def parseAbsolute(timeStr):
|
|
'''
|
|
Parse an absolute time string
|
|
@param timeStr: the time string
|
|
@return the datetime object containing the parsed string
|
|
'''
|
|
options = ["%Y-%jT%H:%M:%S.%f","%Y-%jT%H:%M:%S"]
|
|
return parseTimeStringOption(timeStr, options)
|
|
|
|
descriptor = None
|
|
d = time[0]
|
|
t = time[1:]
|
|
if d == 'R':
|
|
descriptor = Descriptor.RELATIVE
|
|
dt = parseRelative(t)
|
|
delta = timedelta(hours=dt.hour, minutes=dt.minute, seconds=dt.second, microseconds=dt.microsecond).total_seconds()
|
|
elif d == 'A':
|
|
descriptor = Descriptor.ABSOLUTE
|
|
dt = parseAbsolute(t)
|
|
# See if timezone was specified. If not, use UTC
|
|
if dt.tzinfo != None:
|
|
print "Using timezone %s" % dt.tzinfo.tzname()
|
|
epoch = datetime.fromtimestamp(0, dt.tzinfo)
|
|
else:
|
|
print "Using UTC timezone"
|
|
epoch = datetime.utcfromtimestamp(0)
|
|
delta = (dt - epoch).total_seconds()
|
|
else:
|
|
__error(lineNumber, "Invalid time descriptor '" + d + "' found. Descriptor should either be 'A' for absolute times or 'R' for relative times")
|
|
seconds = int(delta)
|
|
useconds = int((delta - seconds) * 1000000)
|
|
return descriptor, seconds, useconds
|
|
|
|
# Open the sequence file and parse each line:
|
|
with open(seqfile, "r") as inputFile:
|
|
for i, line in enumerate(inputFile):
|
|
line = line.strip()
|
|
# ignore blank lines and comments
|
|
if line and line[0] != ';':
|
|
line = removeTrailingComments(line)
|
|
line = splitString(line)
|
|
length = len(line)
|
|
if length < 2:
|
|
__errorLine(i, "Each line must contain a minimum of two fields, time and command mnemonic\n")
|
|
else:
|
|
try:
|
|
descriptor, seconds, useconds = parseTime(i, line[0])
|
|
except:
|
|
__errorLine(i, "Encountered syntax error parsing timestamp")
|
|
mnemonic = line[1]
|
|
args = []
|
|
if length > 2:
|
|
args = line[2:]
|
|
try:
|
|
args = parseArgs(args)
|
|
except:
|
|
__errorLine(i, "Encountered sytax error parsing arguments")
|
|
yield i, descriptor, seconds, useconds, mnemonic, args
|
|
|
|
def generateSequence(inputFile, outputFile=None, timebase=0xffff):
|
|
'''
|
|
Write a binary sequence file from a text sequence file
|
|
@param inputFile: A text input sequence file name (usually a .seq extension)
|
|
@param outputFile: An output binary sequence file name (usually a .bin extension)
|
|
'''
|
|
# Check the user environment:
|
|
try:
|
|
generated_path = os.environ['GSE_GENERATED_PATH']
|
|
generated_command_path = generated_path + "/commands"
|
|
except:
|
|
__error("Environment variable 'GSE_GENERATED_PATH' not set. It should be set to something like '$BUILD_ROOT/Gse/generated/$DEPLOYMENT' and also added to $PYTHONPATH.")
|
|
cmds = command_loader.CommandLoader.getInstance()
|
|
try:
|
|
cmds.create(generated_command_path)
|
|
except gseExceptions.GseControllerUndefinedDirectoryException:
|
|
__error("Environment variable 'GSE_GENERATED_PATH' is set to '" + generated_path + "'. This is not a valid directory. Make sure this variable is set correctly, and the GSE python deployment autocode as been installed in this location.")
|
|
|
|
# Parse the input file:
|
|
command_list = []
|
|
command_obj_dict = cmds.getCommandDict()
|
|
for i, descriptor, seconds, useconds, mnemonic, args in __parse(inputFile):
|
|
# Make sure that command is in the command dictionary:
|
|
if mnemonic in command_obj_dict:
|
|
command_obj = copy.deepcopy(command_obj_dict[mnemonic])
|
|
# Set the command arguments:
|
|
try:
|
|
command_obj.setArgs(args)
|
|
except ArgLengthMismatchException as e:
|
|
__errorLine(i, "'" + mnemonic + "' argument length mismatch. " + e.getMsg())
|
|
except TypeException as e:
|
|
__errorLine(i, "'" + mnemonic + "' argument type mismatch. " + e.getMsg())
|
|
# Set the command time and descriptor:
|
|
command_obj.setDescriptor(descriptor)
|
|
command_obj.setSeconds(seconds)
|
|
command_obj.setUseconds(useconds)
|
|
# Append this command to the command list:
|
|
command_list.append(command_obj)
|
|
else:
|
|
__errorLine(i, "'" + mnemonic + "' does not match any command in the command dictionary.")
|
|
|
|
# Write to the output file:
|
|
writer = SeqBinaryWriter(timebase=timebase)
|
|
if not outputFile:
|
|
outputFile = os.path.splitext(inputFile)[0] + ".bin"
|
|
try:
|
|
writer.open(outputFile)
|
|
except:
|
|
__error("Encountered problem opening output file '" + outputFile + "'.")
|
|
writer.write(command_list)
|
|
writer.close()
|
|
|
|
if __name__ == "__main__":
|
|
'''
|
|
The main program if run from the commandline. Note that this file can also be used
|
|
as a module by calling the generateSequence() function
|
|
'''
|
|
timebase = 0xffff
|
|
good = False
|
|
args = copy.copy(sys.argv[1:])
|
|
try:
|
|
if "-t" in args:
|
|
index = args.index("-t")
|
|
timebase = int(args[index + 1], 0)
|
|
args = args[:index] + args[index + 2:]
|
|
good = True
|
|
except ValueError:
|
|
print "Could not parse time base: {0}".format(args[index + 1])
|
|
except IndexError:
|
|
print "No time base argument supplied to '-t' argument"
|
|
if (len(args) == 1 or len(args) == 2) and good:
|
|
generateSequence(*args, timebase=timebase)
|
|
else:
|
|
print "Usage: tinyseqgen sequence_file_in.seq [binary_sequence_file_out.bin]"
|
|
print
|
|
print "Welcome to tinyseqgen - the Tiny F Prime Command Sequence Generator."
|
|
print
|
|
print "Description: tinyseqgen takes a simple input sequence format (.seq),"
|
|
print "and outputs a binary command sequence loadable by the Svc/SequenceFileLoader."
|
|
print
|
|
print " -t <time base>: time base, set the time base integer. Defaults to: 0xFFFF"
|
|
print
|
|
print "Here, try out this example input sequence file (simple_sequence.seq):"
|
|
print
|
|
print ";--------------------------------------------------------------------"
|
|
print "; Simple sequence file "
|
|
print "; Note: that anything after a ';' is a comment"
|
|
print ";--------------------------------------------------------------------"
|
|
print
|
|
print "; Commands in a sequence can either be timed absolutely or relative "
|
|
print "; to the execution of the previous command. Here is an absolute NOOP"
|
|
print "; command. Make sure to use UTC times, since tinyseqgen will calculate"
|
|
print "; the seconds since the unix epoch from the given absolute time in UTC."
|
|
print "A2015-075T22:32:40.123 CMD_NO_OP"
|
|
print
|
|
print "; Here is a relative NOOP command, which will be run 1 second after"
|
|
print "; the execution of the previous command"
|
|
print "R00:00:01 CMD_NO_OP; Send a no op command"
|
|
print
|
|
print "; This command will run immediately after the previously executed command"
|
|
print "; has completed"
|
|
print "R00:00:00 CMD_NO_OP"
|
|
print
|
|
print "; Let's try out some commands with arguments"
|
|
print "R01:00:01.050 CMD_NO_OP_STRING \"Awesome string!\"; <- cool argument right?"
|
|
print "R03:51:01.000 CMD_TEST_CMD_1 17, 3.2, 2; <- this command has 3 arguments"
|
|
print "R00:05:00 ALOG_SET_EVENT_REPORT_FILTER INPUT_COMMAND, INPUT_DISABLED; <- this command uses enum arguments"
|
|
print
|
|
sys.exit(1)
|
|
sys.exit(0)
|