Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 27 additions & 3 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, e.g. `check("INST HEALTH_STATUS TYPE == '#{expected}'")`.
Comment thread
jmthomas marked this conversation as resolved.
Outdated
:::

<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, e.g. `check("INST HEALTH_STATUS TYPE == '#{expected}'")`.
:::

<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[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'`). 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, e.g. `check("INST HEALTH_STATUS TYPE == '#{expected}'")`.
:::

<Tabs groupId="script-language">
Expand Down
143 changes: 59 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 @@

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,17 @@
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)
comparison = block if block

Check failure on line 740 in openc3/lib/openc3/script/api_shared.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Introduce a new variable instead of reassigning parameter 'comparison'.

See more on https://sonarcloud.io/project/issues?id=OpenC3_cosmos&issues=AaBuXREaRV0_tXUKhu7U&open=AaBuXREaRV0_tXUKhu7U&pullRequest=3832
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
value = nil
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
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 comparison and comparison.call(value)
break if Time.now.sys >= end_time

delta = Time.now.sys - work_start
Expand All @@ -766,58 +755,62 @@

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 comparison and comparison.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 +852,15 @@
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 +869,7 @@
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 +879,19 @@
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