openc3/bin/cstol_converter
#!/usr/bin/env ruby
# encoding: ascii-8bit
# Copyright 2022 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# Modified by OpenC3, Inc.
# All changes Copyright 2022, OpenC3, Inc.
# All Rights Reserved
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.
# This file converts OASIS CSTOL files to OpenC3 scripts
require 'openc3'
require 'openc3/script'
require 'openc3/system'
# TODO: capitalized string from ask statement may not match expression (gmi.rb:70)
# TODO: handle V, C other units specifications...
# TODO: handle unary operators (-1, not - 1) => although Ruby handles this fine
def parse_cmd(words)
# Convert the part that's common to all commands
str = "cmd(\"" + words[1].upcase + " " + words[2].upcase
# If it has parameters
if words.length == 5
str = str + " " + parse_id(words[4], true, false, true, true)
elsif words.length > 5
str = str + " with"
# Join the rest of the list and remove the commas
args = words[4..-1].join(" ").split(",")
args.length.times do |i|
# Only prepend comma if it is not the first argument pair
if i != 0
str = str + ","
end
params = args[i].split(" ")
str = str + " " + params[0].upcase + " " + parse_id(params[1], true, false, true)
end
end
str = str + "\")"
end
def parse_cond_operator(op, is_set_tlm = false)
str = ""
# Convert CSTOL operators into Ruby operators
if op.match?("/=")
str = " != "
elsif op.match?(">=")
str = " >= "
elsif op.match?("<=")
str = " <= "
elsif op.match?(">")
str = " > "
elsif op.match?("<")
str = " < "
elsif op.match?("=")
str = " == "
if is_set_tlm
str = " = "
end
elsif op.match?(/VS/i)
str = " == "
end
str
end
def parse_expression(vars, quoted = false, in_eval = false)
i = 0
str = ""
finished = false
# Make sure there are spaces around the operators
if vars.length > 1
vars = vars.join(" ")
else
vars = vars.to_s
end
vars = vars.gsub("(", " ( ")
vars = vars.gsub(")", " ) ")
vars = vars.gsub("+", " + ")
vars = vars.gsub("-", " - ")
vars = vars.gsub("*", " * ")
vars = vars.gsub("==", " == ")
vars = vars.gsub("/=", " /= ")
vars = vars.gsub("<=", " <= ")
vars = vars.gsub(">=", " >= ")
vars = vars.gsub(";", " ; ")
# Add spaces around single character operators (/ < >)
offset = 0
while (idx = vars.index(/[\/<>][^=]/, offset)) != nil
# Add space before and after operator and increment offset
vars = vars.insert(idx, " ")
vars = vars.insert(idx + 2, " ")
offset = idx + 2
end
# Add spaces around single = operator (not /=, ==, <=, or >=)
offset = 0
while (idx = vars.index(/[^\/=<>]=[^=]/, offset)) != nil
# Add space before and after operator and increment offset
vars = vars.insert(idx + 1, " ")
vars = vars.insert(idx + 3, " ")
offset = idx + 3
end
# Split the expression on spaces
vars = vars.split(" ")
last = nil
while vars[i] != nil and finished != true
case vars[i].tr('[]"', '')
when "("
if quoted == false
str = str + "("
end
when ")"
if quoted == false
str = str + ")"
end
when /\bOR\b/i
str = str + " || "
when /\bAND\b/i
str = str + " && "
when /\b\$[0-9]\b/ # $0, $1, etc are function inputs
str = str + "inVar" + vars[0][1..-1]
when /\$/ # $varName
str = str + parse_id(vars[i].tr('[]"', ''), quoted, in_eval)
when /RAW/i
if quoted
str = str + "tlm_raw('" + parse_tlm_item(vars[i + 1..i + 2]) + "')"
else
str = str + "tlm_raw(\"" + parse_tlm_item(vars[i + 1..i + 2]) + "\")"
end
i = i + 2
when /x#[0-9a-fA-F]+/i # Hex number
str = str + parse_id(vars[i].tr('[]"', ''), quoted)
when /b#[0-1]+/i # Binary number
str = str + parse_id(vars[i].tr('[]"', ''), quoted)
when /(\AVS\z|=|\/=|>|>=|<|<=)/ # Conditional operator
str = str + parse_cond_operator(vars[i])
when /(\+|-|\*|\/)/ # Arithmetic operator
str = str + " " + vars[i].tr('[]"', '') + " "
# Verifies Number followed immediately by units
when /\dDN\z|\ddn\z|\dDEG\z|\ddeg\z|\dRAD\z|\drad\z|\dV\z|\dA\z|\dv\z|\da\z|\dc\z|\dC\z|\df\z|\dF\z|\dDPS\z|\dM\z|\dMPS\z|\dm\z|\dmps\z/ # Checks for decimal/degrees/volts and amps
temp = vars[i].tr('[]"', '')
temp = temp.gsub(/DN\z|dn\z|DEG\z|deg\z|RAD\z|rad\z|V\z|A\z|v\z|a\z|c\z|C\z|f\z|F\z|DPS\z|M\z|MPS\z|m\z|mps\z/, '')
if temp.match?(/[a-zA-Z]/)
str = str + parse_id(vars[i].tr('[]"', ''), quoted)
else
str = str + temp
end
# Verifies units are standalone
when last != '=' && /DN\z|dn\z|DEG\z|deg\z|RAD\z|rad\z|V\z|A\z|v\z|a\z|c\z|C\z|f\z|F\z|DPS\z|M\z|MPS\z|m\z|mps\z/ # Checks for decimal/degrees/volts and amps
# Verify it is not a target
if vars[i] != nil and $targetList.include?(vars[i].tr('[]"', '').upcase)
if quoted
str = str + "tlm('" + parse_tlm_item(vars[i..i + 1]) + "')"
else
str = str + "tlm(\"" + parse_tlm_item(vars[i..i + 1]) + "\")"
end
i = i + 1
else
str = str + parse_id(vars[i].tr('[]"', ''), quoted)
end
when /[0-9]*:[0-9]*:[0-9]+/ # Timestamp
str = str + parse_time(vars[i].tr('[]"', ''))
when /\dE/ # Floating point
temp_number = vars[i].tr('E', '').to_f * (10**vars[i + 2].to_f)
str = str + "#{temp_number}"
finished = true
when /\d/ # Decimal number
str = str + parse_id(vars[i].tr('[]"', ''), quoted)
when /\w/ # Must stay low on the list to avoid matching other items
# Check this keyword against the list of targets
if vars[i] != nil and $targetList.include?(vars[i].tr('[]"', '').upcase)
if quoted
str = str + "tlm('" + parse_tlm_item(vars[i..i + 1]) + "')"
else
str = str + "tlm(\"" + parse_tlm_item(vars[i..i + 1]) + "\")"
end
i += 1
# If it is not a target, then it must be a string or other identifier
else
str = str + parse_id(vars[i].tr('[]"', ''), quoted, false, false, false, false)
end
# Other cases not handled
# else
# str = str + " # TODO unsupported: " + vars[i].tr('[]"', '')
end # case
last = vars[i].tr('[]"', '')
i += 1
end # while
str
end
def parse_id(id, quoted = false, in_eval = false, in_command = false, in_single_command = false, units = true)
str = ""
# Remove parentheses
if id.index("(") == 0
id = id[1..-1]
end
if id.index(")") == id.length - 1
id = id[0..-2]
end
if id.match?(/\$\w/) # $varName
id[1, 1] = id[1, 1].downcase
if quoted and !in_eval
str = "'\#{" + id[1..-1] + "}'"
else
str = id[1..-1]
end
elsif id.match(/^"/) != nil and
id.match(/"$/) != nil # Starts and ends with a quote
if quoted
str = "'" + id.upcase[1..-2] + "'"
else
str = id.upcase
end
elsif id.match?(/x#[A-Fa-f0-9]+DN/i) # Hex number with DN
str = "0x" + id[2..-3]
elsif id.match?(/x#[0-9a-fA-F]+/i) # Hex number
str = "0x" + id[2..-1]
elsif id.match?(/b#[0-1]+DN/i) # Binary number with DN
str = "0b" + id[2..-3]
elsif id.match?(/b#[0-1]+/i) # Binary number
str = "0b" + id[2..-1]
elsif id.match(/\d+DN/i) or id.match(/-\d+DN/i) # Decimal number with DN
str = id[0..-3]
elsif id.match?(/\AVS\z|\Avs\z/)
str = str + " == "
# Verifies extensions with a decimal number followed by a unit
elsif units && id.match(/\dDN|\ddn|\dDEG|\ddeg|\dRAD|\drad|\dV|\dA|\dv|\da|\dc|\dC|\df|\dF|\dDPS|\dM\z|\dMPS\z|\dm\z|\dmps\z/)
temp = id.gsub(/DN\z|dn\z|DEG\z|deg\z|RAD\z|rad\z|V\z|A\z|v\z|a\z|c\z|C\z|f\z|F\z|DPS\z|M\z|MPS\z|m\z|mps\z/, '')
if in_command and temp.match(/[a-zA-Z]/)
str = str + "\'" + id.upcase + "\'"
# if filtered words still contain letter, then it is not a number
elsif temp.match?(/[a-zA-Z]/)
str = str + id
else
str = str + temp
end
elsif id.match?(/\dE\+\d/) # Floating point
id = id.gsub('+', ' + ')
temp_num_arr = id.split(' ')
temp_number = temp_num_arr[0].tr('E', '').to_f * (10**temp_num_arr[2].to_f)
str = str + "#{temp_number}"
elsif id.match(/\A\d/) or id.match(/\A-\d/) # starts with Decimal number
str = id
# Verifies extensions with a decimal number followed by a unit
# that is by itself separated by whitespace
elsif units && id.match(/\ADN\z|\Adn\z|\ADEG\z|\Adeg\z|\ARAD\z|\Arad\z|\AV\z|\AA\z|\Av\z|\Aa\z|\Ac\z|\AC\z|\Af\z|\AF\z|\ADPS\z|\AM\z|\AMPS\z|\Am\z|\Amps\z/)
temp = id.gsub(/DN\z|dn\z|DEG\z|deg\z|RAD\z|rad\z|V\z|A\z|v\z|a\z|c\z|C\z|f\z|F\z|DPS\z|M\z|MPS\z|m\z|mps\z/, '')
if in_command
str = str + "\'" + id.upcase + "\'"
else
str = str + temp
end
elsif id.match?(/\w/) # Any other word
# If it's quoted still need quotes for comparison
if in_single_command
str = id.upcase
elsif quoted
str = "\'" + id.upcase + "\'"
else
str = "\"" + id.tr('\\', '').upcase + "\""
end
else
str = id
end
end
def parse_macro(words)
# Call the macro with the first parameter as binding
str = words[0] + "(binding"
# Add all arguments in a loop
(words.length - 1).times do |i|
str = str + ", " + parse_id(words[i + 1])
end
str = str + ")"
end
def parse_time(time)
# If this is a timestamp
if time.match?(/[0-9]*:[0-9]*:[0-9]+/) # Timestamp
# Split on colons to separate hours/minutes/seconds
tok = time.split(":")
# If the first two tokens are empty, set to 0 (ex. ::10 is a valid time)
if tok[0] == ""
tok[0] = "0"
end
if tok[1] == ""
tok[1] = "0"
end
# Compute the number of seconds
secs = ((3600 * tok[0].to_i) + (60 * tok[1].to_i) + tok[2].to_i).to_s
# Other cases not handled
else
secs = "# TODO unsupported: " + time
end
end
def parse_tlm_item(tlm)
# Remove preceding parentheses
if tlm[0].index("(") == 0
tlm[0] = tlm[0][1..-1]
end
# If there are two telemetry items (target and mnemonic)
if tlm.length == 2
begin
str = tlm[0].tr('[]"', '').upcase + " LATEST " + tlm[1].tr('[]"', '').upcase
rescue
str = "# TODO unknown TLM: " + tlm.to_s
end
# Other cases not handled
else
str = "# TODO unsupported: " + tlm.to_s
end
end
def parse_line(full_line, loc_out_file, wait_check_flag = false)
line = ""
comment = ""
str = ""
# Handle any empty lines
if full_line == nil || full_line.strip == ""
loc_out_file.puts full_line
return
end
# Determine the location of any comments in this line
commentIdx = full_line.index(";")
# If we found a comment operator
if commentIdx != nil
# If there's text to pull out before the ;
if commentIdx != 0
line = full_line[0..(commentIdx - 1)].rstrip
end
# If there's text to pull out after the ;
if commentIdx != full_line.length - 1
comment = "#" + full_line[(commentIdx + 1)..-1]
end
else
# No comment operator so use the entire line
line = full_line.rstrip
end
# Determine the number of spaces to indent this line by finding the first
# non-whitespace character
numSpaces = full_line.index(/\S/)
numSpaces.times { str = str + " " }
# Handle lines with only comments
if line == nil or line == ""
loc_out_file.puts str + comment
return
end
# Redundant substitutions are done for comparators
line = line.gsub("==", " == ")
line = line.gsub("/=", " /= ")
line = line.gsub("<=", " <= ")
line = line.gsub(">=", " >= ")
line = line.gsub(/(\w)(=)(\w)/, '\1 = \3')
line = line.gsub(/(\w)(=)(\d)/, '\1 = \3')
line = line.gsub(/(\w)(=)(-)/, ' = -')
line = line.gsub(/(\w)(=)(\s)/, '\1 = \3')
line = line.gsub(/(\s)(=)(\w)/, '\1 = \3')
# Split the line into tokens by spaces, if no spaces between equals sign add them
words = line.split(" ")
# Check for old CSTOL labels which have a trailing colon
if words[0][-1] == ':'
loc_out_file.puts "# #{line}"
return
end
# if wait_check flag is activated ensure that the next line is a check otherwise infinite loop
if wait_check_flag and words[0].downcase != 'check'
return ""
end
case words[0].downcase
when "endproc"
if $inFunction
str += "end\n#{$inFunction}(#{$inFunctionParams.join(',')})" if $inFunction
$inFunction = nil
end
when "ask"
str = str + parse_id(words[1]) + " = ask(" + words[2]
(words.length - 3).times do |i|
str = str + " " + words[i + 3]
end
str = str + ")"
when "begin"
# Don't need to do anything with this keyword, so exit unless there's
# a comment to print
if comment == ""
return
end
when "check"
# If the next word starts with the raw keyword
if words[1].match?(/\A(raw)/i)
# If the line contains a colon, parse out the range and use tolerance
if line.match?(":")
vsIdx = line.index("VS")
vsIdx = line.index("vs") if vsIdx == nil
colIdx = line.index(":")
lowRange = line[(vsIdx + 2)..(colIdx - 1)]
highRange = line[(colIdx + 1)..-1]
str = str + "check_tolerance_raw(\"" + parse_tlm_item(words[2..3]) +
"\", ((" + parse_expression([highRange], false) + ") + (" +
parse_expression([lowRange], false) +
")) / 2," + " ((" + parse_expression([highRange], false) + ") - (" +
parse_expression([lowRange], false) + "))/2)"
else
if wait_check_flag
if @wait_match_string == "\"" + parse_tlm_item(words[2..3]) +
parse_cond_operator(words[4]) + parse_expression(words[5..-1], true) + "\""
@verify_wait_check = true
# exit function with true flag, and skips check statement next parse
return " "
end
end
str = str + "check_raw(\"" + parse_tlm_item(words[2..3]) +
parse_cond_operator(words[4]) + parse_expression(words[5..-1], true) +
"\")"
end
# If the next word starts with a variable indicator
elsif words[1].match?(/\$/)
# If the line contains a colon, make check expression formatted to
# check against a defined range
if line.match?(":")
vsIdx = line.index("VS")
vsIdx = line.index("vs") if vsIdx == nil
colIdx = line.index(":")
lowRange = line[(vsIdx + 2)..(colIdx - 1)]
highRange = line[(colIdx + 1)..-1]
str = str + "check_expression(\"(" +
parse_expression([words[1]], true) + " >= (" +
parse_expression([lowRange], true) + ")) and (" +
parse_expression([words[1]], true) + " <= (" +
parse_expression([highRange], true) + "))\")"
else
str = str + "check_expression(\"" +
parse_expression(words[1..-1], true) + "\")"
end
# If the next word doesn't start with raw
elsif words[1].match?(/\w/)
# If the line contains a colon, parse out the range and use tolerance
if line.match?(":")
vsIdx = line.index("VS")
vsIdx = line.index("vs") if vsIdx == nil
colIdx = line.index(":")
lowRange = line[(vsIdx + 2)..(colIdx - 1)]
highRange = line[(colIdx + 1)..-1]
str = str + "check_tolerance(\"" +
parse_tlm_item(words[1..2]) + "\", ((" +
parse_expression([highRange], false) + ") + (" +
parse_expression([lowRange], false) + ")) / 2, ((" +
parse_expression([highRange], false) + ") - (" +
parse_expression([lowRange], false) + "))/2)"
else
if words[3]
# if single integer comparison or variable, just parse single (for negative integer cases)
if words.length <= 6
if wait_check_flag
if @wait_match_string == "\"" + parse_tlm_item(words[1..2]) + parse_cond_operator(words[3]) + parse_id(words[4], true) + "\""
@verify_wait_check = true
# exit function with true flag, and skips check statement next parse
return ""
end
else
str = str + "check(\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3]) + parse_id(words[4], true) +
"\")"
end
else
str = str + "check(\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3]) + parse_expression(words[4..-1], true) +
"\")"
end
else
str = str + "check(\"" + parse_tlm_item(words[1..2]) +
"\")"
end
end
end
when "cmd"
str = str + parse_cmd(words)
when "set"
str = str + parse_cmd(words)
when "declare"
# If the next word is input, ignore the line
if words.length >= 2 and words[1].match(/input/i)
if $inFunction
$inFunctionParams << "\"#{words[4].upcase}\""
else
str = str + "# SCL Ignored: " + line
end
# If it is a defined enum list, ignore definitions
elsif words.length >= 6 and
words[4..-1].join(" ").match(/[A-Z]+\s+[A-Z]+(,\s*[A-Z]+)+/i)
str = str + parse_id(words[2]) + " " + words[3] + " " +
parse_id(words[4])
# If it is a defined range (one and only one colon), ignore range
elsif words.length >= 6 and
words[4..-1].join(" ").count(":") == 1
str = str + parse_id(words[2]) + " " + words[3] + " " +
parse_id(words[4])
# Parse the expression
elsif words.length >= 5
str = str + parse_id(words[2]) + " " + words[3] + " " +
parse_expression(words[4..-1])
else
str = str + "# TODO unsupported: " + line
end
when "else"
# Only an else
if words.length == 1
str = str + "else"
# 'Else if' statement
elsif words.length >= 2 and words[1].match(/if/i)
str = str + "elsif " + parse_expression(words[2..-1])
# Other cases not handled
else
str = str + "# TODO unsupported: " + line
end
when "end"
# End of an if statement
if words[1].match?(/if/i)
str = str + "end"
# End of a procedure with arguments
elsif words[1].match(/proc/i) && $inFunction
str += "end\n#{$inFunction}(#{$inFunctionParams.join(',')})"
# End of a procedure without arguments
elsif words[1].match(/proc/i) && !$inFunction
str = str + "# SCL Ignored: " + line
# End of a loop
elsif words[1].match?(/loop/i)
str = str + "end"
# End of a macro
elsif words[1].match?(/macro/i)
str = str + "# SCL Ignored: " + line
# Other cases not handled
else
str = str + "# TODO unsupported: " + line
end
when "endif"
str = str + "end"
when "escape"
str = str + "break"
when "goto"
# Ignore typical goto for skipping the header section
if words.length == 2 and words[1].match(/start_here/i)
str = str + "# SCL Ignored: " + line
# Other cases not handled
else
str = str + "# TODO unsupported: " + line
end
when "if"
str = str + "if " + parse_expression(words[1..-1])
when "let"
# If we're assigning a telemetry point
if $targetList.include?(words[1].upcase)
# If there is no space between the equal sign reparse string
str = str + "set_tlm(\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3], true) + parse_id(words[4], true) + "\")"
else
# there's no spaces between declaration
str = str + parse_id(words[1]) + " " + words[2] + " " +
parse_expression(words[3..-1])
end
when "lock"
# Ignore database commands
str = str + "# SCL Ignored: " + line
when "loop"
# TODO not sure if this is wise, for some files the loop is infinite and
# there's no exist case
if words[1] == nil
str = str + "# TODO Possible infinite loop case check script file" +
"\n" + str + "while(true)"
else
str = str + words[1] + ".times do |i|"
end
when "macro"
str = str + ("# SCL Ignored: " + line)
$macroName = words[1].upcase
$macroNumArgs = 0
i = 2
while (nextWord = words[i]) != nil
case nextWord
when /\$/
$macroNumArgs = $macroNumArgs + 1
end
i = i + 1
end
when "new_mac"
# Ignore OASIS commands
str = str + "# SCL Ignored: " + line
when "new_proc"
# Ignore OASIS commands
str = str + "# SCL Ignored: " + line
when "proc"
# Process procedures without arguments as scripts
if words.length == 2
# Set a global so we know how to close this function
$inFunction = nil
# Process procedures with arguments as functions
else
# Split the list of arguments on commas
listWords = words[2..-1].join().split(",")
# Print the function name and the first argument
str = str + "def " + words[1].downcase + "(" + parse_id(listWords[0])
# Print the remaining arguments preceded by a separator
(listWords.length - 1).times do |i|
str = str + ", " + parse_id(listWords[i + 1])
end
str = str + ")"
# Set a global so we know how to close this function
$inFunction = words[1].downcase
$inFunctionParams = []
end
when "record"
# Record messages without a label
if words.length == 2 and words[1].match(/messages/i)
str = str + "start_logging()\n"
numSpaces.times { str = str + " " }
str = str + "start_new_server_message_log()"
# Record message with a label
elsif words.length == 3 and words[1].match(/messages/i)
str = str + "set_log_label(" + words[2] + ")\n"
numSpaces.times { str = str + " " }
str = str + "start_logging()\n"
numSpaces.times { str = str + " " }
str = str + "start_new_server_message_log()"
# Other cases not handled
else
str = str + "# TODO unsupported: " + line
end
when "restore"
# Ignore database commands
str = str + "# SCL Ignored: " + line
when "run"
temp = words[1..-1].to_s
temp = temp.tr(',[]\"', '')
# removes the quotes symbol remnants
temp = temp.gsub('\\', '')
temp = temp.delete_suffix('\\')
str = str + "system(\'" + temp + "\')"
when "start"
# Process procedures without arguments as scripts
if words.length == 2
str = str + "start(\"" + words[1].downcase + ".rb\")"
# Process procedures with arguments as functions
else
# Add the statement to require the function
str = str + "load_utility(\"" + words[1].downcase + ".rb\")\n"
numSpaces.times { str = str + " " }
# Split the list of arguments on commas
listWords = words[2..-1].join().split(",")
listWords.map! do |word|
if word.match?(/[0-9]*:[0-9]*:[0-9]+/) # Timestamp
parse_time(word)
else
word
end
end
# Print the function name and the first argument
str = str + words[1].downcase + "(" + parse_id(listWords[0])
# Print the remaining arguments preceded by a separator
(listWords.length - 1).times do |i|
str = str + ", " + parse_id(listWords[i + 1])
end
str = str + ")"
end
when "start_here:"
# Ignore typical starting point label
str = str + "# SCL Ignored: " + line
when "stop"
# Ignore calls to stop logging since OpenC3 stops logging with each start
str = str + "# SCL Ignored: " + line
when "unlock"
# Ignore database commands
str = str + "# SCL Ignored: " + line
when "update"
# Ignore database commands
str = str + "# SCL Ignored: " + line
when "wait"
# add a space in case there's no space between or and parentheses
line = line.gsub(')or', ') or')
words = line.split(' ')
# Only a wait
if words.length == 1
str = str + "wait()"
# Waiting for length of time
elsif words[1].match?(/[0-9]*:[0-9]*:[0-9]+/)
str = str + "wait(" + parse_time(words[1]) + ")"
# Waiting for a variable of time
elsif words.length == 2 and words[1].match(/\$/)
str = str + "wait(" + parse_id(words[1]) + ")"
# Waiting for an expression [or for a time]
elsif words.length > 2 and words[1].match(/\$/)
# Get index of 'OR FOR' if it exists by the 'FOR' keyword
idx = words.index("FOR")
if idx == nil
idx = words.index("for")
end
# If there is a timeout
if idx != nil
str = str + "wait_expression(\"" +
parse_expression(words[1..(idx - 2)], true) + "\", " +
parse_expression([words[idx + 1]]) + ")"
# No timeout given, so insert a default timeout
else
str = str + "wait_expression(\"" +
parse_expression(words[1..-1], true) + "\", " +
parse_time("::30") + ")"
end
# If the next word starts with the raw keyword
elsif words[1].match?(/[(]*(raw)/i)
# Get index of 'OR FOR' if it exists by the 'FOR' keyword
idx = words.index("FOR")
if idx == nil
idx = words.index("for")
end
# If there is a timeout
if idx != nil
# If the first part is a complex expression
if words[1..(idx - 2)].include?("OR") ||
words[1..(idx - 2)].include?("or") ||
words[1..(idx - 2)].include?("AND") ||
words[1..(idx - 2)].include?("and")
str = str + "wait_expression(\"" +
parse_expression(words[1..(idx - 2)], true) + "\", "
# If it is a single telemetry item
else
@wait_match_string = "\"" + parse_tlm_item(words[2..3]) +
parse_cond_operator(words[4]) +
parse_expression(words[5..(idx - 2)], true) + "\""
@verify_wait_check = false
parse_line(@data_by_lines[@universal_index + 1], @out_file, true)
if @verify_wait_check == true
str = str + "wait_check_raw(\"" + parse_tlm_item(words[2..3]) +
parse_cond_operator(words[4]) +
parse_expression(words[5..(idx - 2)], true) + "\", "
else
str = str + "wait_raw(\"" + parse_tlm_item(words[2..3]) +
parse_cond_operator(words[4]) +
parse_expression(words[5..(idx - 2)], true) + "\", "
end
end
# Parse the timeout
str = str + parse_expression([words[idx + 1]])
# If there is no timeout given
else
# If it is a complex expression
if words[1..-1].include?("OR") || words[1..-1].include?("or") ||
words[1..-1].include?("AND") || words[1..-1].include?("and")
str = str + "wait_expression(\"" +
parse_expression(words[1..-1], true) + "\", "
# If it is a single telemetry item
else
@wait_match_string = "\"" + parse_tlm_item(words[2..3]) +
parse_cond_operator(words[4]) + parse_id(words[5], true) + "\""
@verify_wait_check = false
parse_line(@data_by_lines[@universal_index + 1], @out_file, true)
if @verify_wait_check == true
str = str + "wait_check_raw(\"" + parse_tlm_item(words[2..3]) +
parse_cond_operator(words[4]) + parse_id(words[5], true) + "\", "
else
str = str + "wait_raw(\"" + parse_tlm_item(words[2..3]) +
parse_cond_operator(words[4]) + parse_id(words[5], true) + "\", "
end
end
# Insert a default timeout
str = str + parse_time("::30")
end
str = str + ")"
# If the next word doesn't start with raw
elsif words[1].match?(/[(]*\w/)
# Get index of 'OR FOR' if it exists by the 'FOR' keyword
idx = words.index("FOR")
if idx == nil
idx = words.index("for")
end
# If there is a timeout
if idx != nil
# If the first part is a complex expression
if words[1..(idx - 2)].include?("OR") ||
words[1..(idx - 2)].include?("or") ||
words[1..(idx - 2)].include?("AND") ||
words[1..(idx - 2)].include?("and")
str = str + "wait_expression(\"" +
parse_expression(words[1..(idx - 2)], true) + "\", "
# If it is a single telemetry item
else
@wait_match_string = "\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3]) + parse_id(words[4], true) + "\""
@verify_wait_check = false
parse_line(@data_by_lines[@universal_index + 1], @out_file, true)
if @verify_wait_check == true
str = str + "wait_check(\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3]) + parse_id(words[4], true) + "\", "
else
str = str + "wait(\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3]) + parse_id(words[4], true) + "\", "
end
end
# Parse the timeout
str = str + parse_expression([words[idx + 1]])
# If there is no timeout given
else
# If it is a complex expression
if words[1..-1].include?("OR") || words[1..-1].include?("or") ||
words[1..-1].include?("AND") || words[1..-1].include?("and")
str = str + "wait_expression(\"" +
parse_expression(words[1..-1], true) + "\", "
# If it is a single telemetry item
else
@wait_match_string = "\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3]) + parse_id(words[4], true) + "\""
@verify_wait_check = false
parse_line(@data_by_lines[@universal_index + 1], @out_file, true)
if @verify_wait_check == true
str = str + "wait_check(\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3]) + parse_id(words[4], true) + "\", "
else
str = str + "wait(\"" + parse_tlm_item(words[1..2]) +
parse_cond_operator(words[3]) + parse_id(words[4], true) + "\", "
end
end
# Insert a default timeout
str = str + parse_time("::30")
end
str = str + ")"
else
str = str + "# TODO unsupported: " + line
end
when "write"
# Write the command and first word
str = str + "puts(" + words[1]
# Consolidate a " , to ",
rem = words[2..-1].join(" ")
quoteSpaceIdx = rem.index("\" ,")
if quoteSpaceIdx != nil
rem = rem[0..(quoteSpaceIdx)] + rem[(quoteSpaceIdx + 2)..-1]
end
# Find the closing quote
quoteIdx = rem.index("\",")
# If there was a closing quote
if quoteIdx != nil and quoteIdx != rem.length - 1
# Write the characters up until the quote
str = str + rem[0..quoteIdx - 1]
# Parse the rest after the ", as a quoted expression
expr = rem[(quoteIdx + 2)..-1]
if expr.class == String
# There might be yet another quoted section
quoteIdx = expr.index("\"")
remainder = ''
if quoteIdx
remainder = expr[quoteIdx + 1..-2]
expr = expr[0..quoteIdx - 1].strip
expr = expr[0..-2] if expr[-1] == ','
end
str = str + " \#{#{parse_expression([expr], true, true)}}#{remainder}\""
else
puts "****************** NOT STRING ******************"
str = str + " \#{" + parse_expression(expr, true, true) + "}\""
end
# If there was no closing quote
else
words[2..-1].each do |word|
str = str + " " + word.to_s
end
end
str = str + ")"
else
# If this keyword is contained in the list of macros
if $macroList != nil and $macroList.include?(words[0].upcase)
str = str + parse_macro(words)
# Other keywords not handled
else
str = str + "# TODO unsupported: " + line
end
end
# Implicit end of case statement
# Write the code and comment to the output file
if comment == ""
loc_out_file.puts str
else
loc_out_file.puts str + " " + comment
end
end
################################################################################
##### BEGIN SCRIPT ######
################################################################################
require 'ostruct'
require 'optparse'
options = OpenStruct.new
options.file = nil
options.scope = 'DEFAULT'
opts = OptionParser.new do |opts|
opts.banner = "Usage: cstol_converter [optional filenames]"
opts.separator ""
opts.separator "By default it will parse all macros (*.mac) and CSTOLS (*.prc)"
opts.separator "recursively starting in the current working directory"
opts.separator ""
# Create the help and version options
opts.on("-h", "--help", "Show this message") do
puts opts
exit
end
opts.on("-s SCOPE", "--scope SCOPE", "Use the specified scope instead of DEFAULT") do |arg|
options.scope = arg
end
end
begin
opts.parse!(ARGV)
rescue => e
puts e
puts opts
exit
end
mac_files = []
prc_files = []
if ARGV[0]
ARGV.each do |filename|
prc_files << filename
end
else
# Find all macros
mac_files = Dir["**/*.mac"]
# Find all procedures
prc_files = Dir["**/*.prc"]
end
# List of targets found in the CSTOL files
$targetList = OpenC3::TargetModel.names(scope: options.scope)
# Process all macros first
unless mac_files.empty?
puts "*****************************************************"
macros_file = File.open("macrosAutoGen.rb", "w")
mac_files.each do |file|
puts " Parsing MAC file: " + file
# Parse each line in the macro file
File.open(file, "r") do |infile|
out_file = File.open(File.join(Dir.pwd, file[0..-5] + "_macro.rb"), "w")
infile.each do |line|
parse_line(line, @out_file)
end
@out_file.close
end
# Create a Ruby macro file that evaluates the macro in the callers context
macros_file.print "#{$macroName}_SRC =
open(\"#{file[0..-5]}_macro.rb\"){ |f|\n f.sysread(f.stat().size())\n}\n\n"
macros_file.print "def #{$macroName}(locBinding"
$macroNumArgs.times { |num|
macros_file.print ",macVar#{num + 1}"
}
macros_file.print ")\n"
$macroNumArgs.times { |num|
macros_file.print " eval(\"inVar#{num + 1} = \#{macVar#{num + 1}}\",locBinding)\n"
}
macros_file.print " eval(#{$macroName}_SRC,locBinding)\n"
macros_file.print "end\n\n"
# Append this macro to the master list of macros
if $macroList == nil
$macroList = [$macroName]
else
$macroList = $macroList.concat([$macroName])
end
end
macros_file.close
end
if prc_files.empty?
puts "No *.prc files found"
else
puts "*****************************************************"
# Process all procedures next
prc_files.each do |file|
puts " Parsing PRC file: " + file
# Open each procedure
File.open(file, "r") do |infile|
# Open its equivalent Ruby output file
@out_file = File.open(File.join(Dir.pwd, File.basename(file)[0..-5] + ".rb"), "w")
# Read the entire file in first in order to compress line continuations
@data = ""
matched_quotes = true
infile.each do |line|
# Check for matching quotes on this line
num_quotes = line.scan(/("|[^\\]")/).size
if num_quotes % 2 == 1
matched_quotes = !matched_quotes
end
# If the last non-whitespace character is the line continuation char
if line.match?(/&\s*$/)
# Remove the continuation char and returns to join with next line
idx = line.rindex("&")
@data = @data + line[0..(idx - 1)]
elsif matched_quotes == false
# If the unmatched string uses the non-standard underscore char
if line.match?(/_\s*$/)
# Remove the trailing underscore
idx = line.rindex("_")
line = line[0..(idx - 1)]
end
# Remove the returns from this line to join with next line
@data = @data + line.rstrip
else
# Leave it alone
@data = @data + line
end
end
@universal_index = 0
@data_by_lines = @data.lines.to_a
# Parse each line in the file
@data_by_lines.each do |_line|
if @verify_wait_check == true
# Skip line
@verify_wait_check = false
else
parse_line(@data_by_lines[@universal_index], @out_file)
end
@universal_index += 1
end
# Close the file
@out_file.close
end
end
end