Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions docs.openc3.com/docs/guides/scripting-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2684,8 +2684,16 @@ check("<Target Name> <Packet Name> <Item Name> <Comparison - optional>")
| Item Name | Name of the telemetry item. |
| Comparison | A comparison to perform against the telemetry item. If a comparison is not given then the telemetry item will just be printed into the script log. |

:::note[Supported Comparisons]
A comparison is a single operator followed by a literal value. The supported operators are
`==`, `!=`, `>`, `>=`, `<`, `<=` and `in`. `in` requires a list operand, e.g. `in [1, 2, 3]`,
whose elements follow the same rules as any other value, e.g. `in ['ON', 'OFF']`.
Compound expressions, e.g. `TIMEUS & 0x0001 == 0x0000`, are not supported - use
[check_expression](#check_expression) instead.
:::

:::note[String Comparisons]
When comparing against string or state values, the value must be quoted (e.g., `== 'ON'`). Unquoted values are interpreted as variable names.
When comparing against string or state values, the value must be quoted (e.g., `== 'ON'`). An unquoted value is rejected with `Uninitialized constant ON. Did you mean 'ON' as a string?`. Quoted values follow the string literal rules of the script language, so escape sequences are processed in Ruby double quoted strings and in all Python strings. The Ruby control and meta escapes `\c`, `\C-` and `\M-` are rejected rather than silently changed, as is string interpolation (Ruby `"#{...}"`, Python f-strings) because the comparison is not evaluated as code. Interpolate in the script itself instead: Ruby `check("INST HEALTH_STATUS TYPE == '#{expected}'")` or Python `check(f"INST HEALTH_STATUS TYPE == '{expected}'")`.
:::

<Tabs groupId="script-language">
Expand Down Expand Up @@ -4143,8 +4151,16 @@ success = wait(
| type | Named parameter specifying the type. RAW, CONVERTED (default) or FORMATTED (Ruby symbol, Python string). |
| quiet | Named parameter indicating whether to log the result. Defaults to false which means log the wait. |

:::note[Supported Comparisons]
A comparison is a single operator followed by a literal value. The supported operators are
`==`, `!=`, `>`, `>=`, `<`, `<=` and `in`. `in` requires a list operand, e.g. `in [1, 2, 3]`,
whose elements follow the same rules as any other value, e.g. `in ['ON', 'OFF']`.
Compound expressions, e.g. `TIMEUS & 0x0001 == 0x0000`, are not supported - use
[wait_expression](#wait_expression) instead.
:::

:::note[String Comparisons]
When comparing against string or state values, the value must be quoted (e.g., `== 'ON'`). Unquoted values are interpreted as variable names.
When comparing against string or state values, the value must be quoted (e.g., `== 'ON'`). An unquoted value is rejected with `Uninitialized constant ON. Did you mean 'ON' as a string?`. Quoted values follow the string literal rules of the script language, so escape sequences are processed in Ruby double quoted strings and in all Python strings. The Ruby control and meta escapes `\c`, `\C-` and `\M-` are rejected rather than silently changed, as is string interpolation (Ruby `"#{...}"`, Python f-strings) because the comparison is not evaluated as code. Interpolate in the script itself instead: Ruby `wait("INST HEALTH_STATUS TYPE == '#{expected}'", 10)` or Python `wait(f"INST HEALTH_STATUS TYPE == '{expected}'", 10)`.
:::

<Tabs groupId="script-language">
Expand Down Expand Up @@ -4424,8 +4440,16 @@ elapsed = wait_check(
| Polling Rate | How often the comparison is evaluated in seconds. Defaults to 0.25 if not specified. |
| type | Named parameter specifying the type. RAW, CONVERTED (default) or FORMATTED (Ruby symbol, Python string). |

:::note String Comparisons
When comparing against string or state values, the value must be quoted (e.g., `== 'ON'`). Unquoted values are interpreted as variable names.
:::note[Supported Comparisons]
A comparison is a single operator followed by a literal value. The supported operators are
`==`, `!=`, `>`, `>=`, `<`, `<=` and `in`. `in` requires a list operand, e.g. `in [1, 2, 3]`,
whose elements follow the same rules as any other value, e.g. `in ['ON', 'OFF']`.
Compound expressions, e.g. `TIMEUS & 0x0001 == 0x0000`, are not supported - use
[wait_check_expression](#wait_check_expression) instead.
:::

:::note[String Comparisons]
When comparing against string or state values, the value must be quoted (e.g., `== 'ON'`). An unquoted value is rejected with `Uninitialized constant ON. Did you mean 'ON' as a string?`. Quoted values follow the string literal rules of the script language, so escape sequences are processed in Ruby double quoted strings and in all Python strings. The Ruby control and meta escapes `\c`, `\C-` and `\M-` are rejected rather than silently changed, as is string interpolation (Ruby `"#{...}"`, Python f-strings) because the comparison is not evaluated as code. Interpolate in the script itself instead: Ruby `wait_check("INST HEALTH_STATUS TYPE == '#{expected}'", 10)` or Python `wait_check(f"INST HEALTH_STATUS TYPE == '{expected}'", 10)`.
:::

<Tabs groupId="script-language">
Expand Down
142 changes: 58 additions & 84 deletions openc3/lib/openc3/script/api_shared.rb
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ def _check(*args, scope: $openc3_scope, token: $openc3_token)

value = yield(target_name, packet_name, item_name)
if comparison_to_eval
_check_eval(target_name, packet_name, item_name, comparison_to_eval, value)
_check_comparison(target_name, packet_name, item_name, comparison_to_eval, value)
else
puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value.nil? ? 'nil' : value.inspect}"
end
Expand Down Expand Up @@ -733,28 +733,16 @@ def _wait_check_process_args(args)
return [target_name, packet_name, item_name, comparison_to_eval, timeout, polling_rate]
end

def _openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, exp_to_eval, scope: $openc3_scope, token: $openc3_token, &block)
# Waits for the comparison to be true or the timeout to expire.
# The comparison is a callable which takes the telemetry value and returns true or false.
# A block passed by the user takes precedence over the comparison.
def _openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: $openc3_scope, token: $openc3_token, &block)
condition = block || comparison
end_time = Time.now.sys + timeout
if exp_to_eval and !exp_to_eval.is_printable?
raise "ERROR: Invalid comparison to non-ascii value"
end
while true
work_start = Time.now.sys
value = tlm(target_name, packet_name, item_name, type: value_type, scope: scope, token: token)
if not block.nil?
if block.call(value)
return true, value
end
else
begin
if eval(exp_to_eval)
return true, value
end
# NoMethodError is raised when the tlm() returns nil and we try to eval the expression
# In this case we just continue and see if eventually we get a good value from tlm()
rescue NoMethodError
end
end
return true, value if condition and condition.call(value)
break if Time.now.sys >= end_time

delta = Time.now.sys - work_start
Expand All @@ -766,58 +754,62 @@ def _openc3_script_wait_implementation(target_name, packet_name, item_name, valu

if canceled
value = tlm(target_name, packet_name, item_name, type: value_type, scope: scope, token: token)
if not block.nil?
if block.call(value)
return true, value
else
return false, value
end
if condition and condition.call(value)
return true, value
else
begin
if eval(exp_to_eval)
return true, value
else
return false, value
end
# NoMethodError is raised when the tlm() returns nil and we try to eval the expression
rescue NoMethodError
return false, value
end
return false, value
end
end
end

return false, value
rescue NameError => e
if e.message =~ /uninitialized constant OpenC3::ApiShared::(\w+)/
new_error = NameError.new("Uninitialized constant #{$1}. Did you mean '#{$1}' as a string?")
new_error.set_backtrace(e.backtrace)
raise new_error
else
raise e
end
end

# Builds a callable which compares a telemetry value against the given comparison string,
# e.g. "> 1". Returns nil if there is no comparison, e.g. a block based wait_check().
# Raises if the comparison is invalid, e.g. an unsupported operator or unparsable operand.
def _comparison_implementation(comparison_to_eval)
return nil unless comparison_to_eval

operator, operand = extract_operator_and_operand_from_comparison(comparison_to_eval)
return nil unless operator
lambda { |value| compare_values(value, operator, operand) }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lambda is a type of proc which is callable

end

# Wait for a converted telemetry item to pass a comparison
def _openc3_script_wait_implementation_comparison(target_name, packet_name, item_name, value_type, comparison_to_eval, timeout, polling_rate = DEFAULT_TLM_POLLING_RATE, scope: $openc3_scope, token: $openc3_token, &block)
if comparison_to_eval
exp_to_eval = "value " + comparison_to_eval
else
exp_to_eval = nil
# The comparison text is logged whether or not it is parsed so it is always validated
if comparison_to_eval and !comparison_to_eval.is_printable?
raise "ERROR: Invalid comparison to non-ascii value"
end
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, exp_to_eval, scope: scope, token: token, &block)
# A user supplied block is the condition so the comparison text is never parsed
comparison = block ? nil : _comparison_implementation(comparison_to_eval)
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
end

def _openc3_script_wait_implementation_tolerance(target_name, packet_name, item_name, value_type, expected_value, tolerance, timeout, polling_rate = DEFAULT_TLM_POLLING_RATE, scope: $openc3_scope, token: $openc3_token, &block)
exp_to_eval = "((#{expected_value} - #{tolerance})..(#{expected_value} + #{tolerance})).include? value"
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, exp_to_eval, scope: scope, token: token, &block)
comparison = lambda do |value|
begin
value >= (expected_value - tolerance) and value <= (expected_value + tolerance)
rescue ArgumentError, NoMethodError, TypeError
false
end
end
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
end

def _openc3_script_wait_implementation_array_tolerance(array_size, target_name, packet_name, item_name, value_type, expected_value, tolerance, timeout, polling_rate = DEFAULT_TLM_POLLING_RATE, scope: $openc3_scope, token: $openc3_token, &block)
statements = []
array_size.times { |i| statements << "(((#{expected_value[i]} - #{tolerance[i]})..(#{expected_value[i]} + #{tolerance[i]})).include? value[#{i}])" }
exp_to_eval = statements.join(" && ")
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, exp_to_eval, scope: scope, token: token, &block)
comparison = lambda do |values|
begin
next false unless values.is_a?(Array) and values.length == array_size
array_size.times.all? do |i|
values[i] >= (expected_value[i] - tolerance[i]) and values[i] <= (expected_value[i] + tolerance[i])
end
rescue ArgumentError, NoMethodError, TypeError
false
end
end
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
end

# Wait on an expression to be true.
Expand Down Expand Up @@ -859,17 +851,15 @@ def _openc3_script_wait_implementation_expression(exp_to_eval, timeout, polling_
end
end

def _check_eval(target_name, packet_name, item_name, comparison_to_eval, value)
string = "value " + comparison_to_eval
def _check_comparison(target_name, packet_name, item_name, comparison_to_eval, value)
check_str = "CHECK: #{_upcase(target_name, packet_name, item_name)} #{comparison_to_eval}"
# Show user the check against a quoted string
# Note: We have to preserve the original 'value' variable because we're going to eval against it
value_str = value.is_a?(String) ? "'#{value}'" : value
value_str = 'nil' if value.nil? # Show user nil value as 'nil'
with_value = "with value == #{value_str}"

eval_is_valid = _check_eval_validity(value, comparison_to_eval)
unless eval_is_valid
operator, operand = extract_operator_and_operand_from_comparison(comparison_to_eval)
unless _valid_comparison?(value, operator, operand)
message = "Invalid comparison for types"
if $disconnect
puts "ERROR: #{message}"
Expand All @@ -878,7 +868,7 @@ def _check_eval(target_name, packet_name, item_name, comparison_to_eval, value)
end
end

if eval_is_valid && eval(string)
if compare_values(value, operator, operand)
puts "#{check_str} success #{with_value}"
else
message = "#{check_str} failed #{with_value}"
Expand All @@ -888,35 +878,19 @@ def _check_eval(target_name, packet_name, item_name, comparison_to_eval, value)
raise CheckError, message
end
end
rescue NameError => e
if e.message =~ /uninitialized constant OpenC3::ApiShared::(\w+)/
new_error = NameError.new("Uninitialized constant #{$1}. Did you mean '#{$1}' as a string?")
new_error.set_backtrace(e.backtrace)
raise new_error
else
raise e
end
end

def _check_eval_validity(value, comparison)
return true if comparison.nil? || comparison.empty?

begin
operator, operand = extract_operator_and_operand_from_comparison(comparison)
rescue RuntimeError => e
if e.message.include?("Unable to parse operand")
# If we can't parse the operand, let the eval happen anyway
# It will raise an appropriate error (like NameError for undefined constants)
return true
end
raise # Re-raise invalid operator errors
rescue JSON::ParserError
return true
end
# Returns whether the value and operand can be meaningfully compared with the operator.
# Note this is only used by check() because wait() polls until the value changes.
def _valid_comparison?(value, operator, operand)
return true if operator.nil?

if [">=", "<=", ">", "<"].include?(operator)
return false if value.nil? || operand.nil? || value.is_a?(Array) || operand.is_a?(Array)
return false if value.nil? or operand.nil?
return false if value.is_a?(Array) or operand.is_a?(Array)
return false if value.is_a?(String) != operand.is_a?(String)
end
# Note 'in' does not need a check here because the parser already requires a list operand

return true
end
Expand Down
Loading
Loading