openc3 7.4.0 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/openc3cli +4 -3
- data/bin/pipinstall +18 -5
- data/bin/pipuninstall +1 -1
- data/bin/uvinstall +36 -8
- data/data/config/default_plugins.yaml +43 -0
- data/data/config/interface_modifiers.yaml +22 -7
- data/data/config/item_modifiers.yaml +14 -4
- data/data/config/plugins.yaml +1 -1
- data/data/config/widgets.yaml +1 -1
- data/lib/openc3/api/cmd_api.rb +10 -13
- data/lib/openc3/api/interface_api.rb +1 -2
- data/lib/openc3/api/limits_api.rb +1 -1
- data/lib/openc3/api/metrics_api.rb +26 -1
- data/lib/openc3/api/router_api.rb +1 -2
- data/lib/openc3/api/tlm_api.rb +21 -21
- data/lib/openc3/config/config_parser.rb +3 -1
- data/lib/openc3/core_ext/tempfile.rb +2 -0
- data/lib/openc3/interfaces/file_interface.rb +6 -1
- data/lib/openc3/interfaces/http_client_interface.rb +14 -6
- data/lib/openc3/interfaces/interface.rb +27 -3
- data/lib/openc3/interfaces/protocols/protocol.rb +8 -0
- data/lib/openc3/interfaces/read_queue_interface.rb +331 -0
- data/lib/openc3/interfaces/stream_interface.rb +22 -3
- data/lib/openc3/interfaces/tcpip_server_interface.rb +13 -0
- data/lib/openc3/interfaces/udp_interface.rb +16 -2
- data/lib/openc3/interfaces.rb +1 -0
- data/lib/openc3/io/json_api_object.rb +24 -3
- data/lib/openc3/io/json_drb_object.rb +3 -2
- data/lib/openc3/logs/log_writer.rb +13 -4
- data/lib/openc3/microservices/decom_microservice.rb +7 -4
- data/lib/openc3/microservices/interface_microservice.rb +146 -50
- data/lib/openc3/microservices/log_microservice.rb +1 -3
- data/lib/openc3/microservices/microservice.rb +25 -1
- data/lib/openc3/microservices/plugin_microservice.rb +32 -0
- data/lib/openc3/microservices/text_log_microservice.rb +1 -3
- data/lib/openc3/models/interface_model.rb +1 -1
- data/lib/openc3/models/interface_status_model.rb +6 -0
- data/lib/openc3/models/metric_model.rb +52 -4
- data/lib/openc3/models/microservice_model.rb +7 -3
- data/lib/openc3/models/plugin_model.rb +12 -12
- data/lib/openc3/models/python_package_model.rb +3 -13
- data/lib/openc3/models/reaction_model.rb +6 -3
- data/lib/openc3/models/setting_model.rb +5 -3
- data/lib/openc3/models/target_model.rb +83 -9
- data/lib/openc3/operators/microservice_operator.rb +5 -0
- data/lib/openc3/operators/operator.rb +69 -0
- data/lib/openc3/packets/packet.rb +10 -4
- data/lib/openc3/packets/packet_config.rb +48 -2
- data/lib/openc3/packets/packet_item.rb +27 -14
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/parsers/xtce_parser.rb +8 -1
- data/lib/openc3/packets/structure_item.rb +3 -2
- data/lib/openc3/script/api_shared.rb +58 -84
- data/lib/openc3/script/extract.rb +215 -19
- data/lib/openc3/script/plugins.rb +4 -11
- data/lib/openc3/script/storage.rb +81 -10
- data/lib/openc3/script/tables.rb +5 -1
- data/lib/openc3/system/system.rb +9 -6
- data/lib/openc3/topics/command_decom_topic.rb +1 -1
- data/lib/openc3/topics/command_topic.rb +2 -1
- data/lib/openc3/topics/router_topic.rb +2 -1
- data/lib/openc3/topics/telemetry_decom_topic.rb +1 -1
- data/lib/openc3/topics/telemetry_topic.rb +3 -2
- data/lib/openc3/topics/topic.rb +154 -2
- data/lib/openc3/utilities/cli_generator.rb +67 -15
- data/lib/openc3/utilities/config_overlay.rb +35 -17
- data/lib/openc3/utilities/default_plugins.rb +33 -0
- data/lib/openc3/utilities/local_mode.rb +82 -46
- data/lib/openc3/utilities/metric.rb +16 -0
- data/lib/openc3/utilities/process_stats.rb +160 -0
- data/lib/openc3/utilities/pypi_url.rb +65 -0
- data/lib/openc3/utilities/questdb_client.rb +43 -46
- data/lib/openc3/utilities/running_script.rb +22 -18
- data/lib/openc3/utilities/store_autoload.rb +20 -3
- data/lib/openc3/utilities/store_queued.rb +37 -9
- data/lib/openc3/utilities/target_file.rb +57 -12
- data/lib/openc3/utilities/topic_lag_monitor.rb +193 -0
- data/lib/openc3/version.rb +5 -5
- data/templates/conversion/conversion.py +6 -2
- data/templates/conversion/conversion.rb +5 -0
- data/templates/plugin/.github/workflows/playwright.yml +62 -0
- data/templates/plugin/.github/workflows/release.yml +45 -0
- data/templates/plugin/.github/workflows/unit-tests.yml +35 -0
- data/templates/plugin/.gitignore +6 -0
- data/templates/plugin/README.md +114 -2
- data/templates/plugin/lib/PACKAGE/__init__.py +4 -0
- data/templates/plugin/plugin.gemspec +10 -1
- data/templates/plugin/pyproject.toml +119 -0
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_angular/pnpm-workspace.yaml +16 -0
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_react/pnpm-workspace.yaml +16 -0
- data/templates/tool_svelte/package.json +2 -1
- data/templates/tool_svelte/pnpm-workspace.yaml +20 -0
- data/templates/tool_vue/package.json +3 -3
- data/templates/tool_vue/pnpm-workspace.yaml +16 -0
- data/templates/widget/package.json +2 -2
- data/templates/widget/pnpm-workspace.yaml +16 -0
- metadata +36 -1
|
@@ -85,8 +85,22 @@ module OpenC3
|
|
|
85
85
|
# The allowable state colors
|
|
86
86
|
STATE_COLORS = [:GREEN, :YELLOW, :RED]
|
|
87
87
|
|
|
88
|
+
# PacketItemLimits is created on first access since most items have no
|
|
89
|
+
# limits and large definitions have hundreds of thousands of items.
|
|
88
90
|
# @return [PacketItemLimits] All information regarding limits for this PacketItem
|
|
89
|
-
|
|
91
|
+
def limits
|
|
92
|
+
@limits ||= PacketItemLimits.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Symbol, nil] The limits state without creating PacketItemLimits
|
|
96
|
+
def limits_state
|
|
97
|
+
@limits ? @limits.state : nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Hash, nil] The limits values without creating PacketItemLimits
|
|
101
|
+
def limits_values
|
|
102
|
+
@limits ? @limits.values : nil
|
|
103
|
+
end
|
|
90
104
|
|
|
91
105
|
# @return [Boolean] Whether the parameter must be obfuscated from logs or not
|
|
92
106
|
attr_accessor :obfuscate
|
|
@@ -110,7 +124,7 @@ module OpenC3
|
|
|
110
124
|
@obfuscate = false
|
|
111
125
|
@messages_disabled = nil
|
|
112
126
|
@state_colors = nil
|
|
113
|
-
@limits =
|
|
127
|
+
@limits = nil
|
|
114
128
|
@meta = nil
|
|
115
129
|
end
|
|
116
130
|
|
|
@@ -165,7 +179,6 @@ module OpenC3
|
|
|
165
179
|
end
|
|
166
180
|
|
|
167
181
|
@states = upcase_states
|
|
168
|
-
@state_colors ||= {}
|
|
169
182
|
else
|
|
170
183
|
@states = nil
|
|
171
184
|
end
|
|
@@ -317,7 +330,7 @@ module OpenC3
|
|
|
317
330
|
item.hazardous = self.hazardous.clone if self.hazardous
|
|
318
331
|
item.messages_disabled = self.messages_disabled.clone if self.messages_disabled
|
|
319
332
|
item.state_colors = self.state_colors.clone if self.state_colors
|
|
320
|
-
item.limits =
|
|
333
|
+
item.limits = @limits.clone if @limits
|
|
321
334
|
item.meta = self.meta.clone if @meta
|
|
322
335
|
item.obfuscate = self.obfuscate.clone if @obfuscate
|
|
323
336
|
item
|
|
@@ -401,16 +414,16 @@ module OpenC3
|
|
|
401
414
|
config << self.read_conversion.to_config(:READ) if self.read_conversion
|
|
402
415
|
config << self.write_conversion.to_config(:WRITE) if self.write_conversion
|
|
403
416
|
|
|
404
|
-
if
|
|
405
|
-
|
|
406
|
-
config << " LIMITS #{limits_set} #{
|
|
417
|
+
if limits_values
|
|
418
|
+
@limits.values.each do |limits_set, limits_values|
|
|
419
|
+
config << " LIMITS #{limits_set} #{@limits.persistence_setting} #{@limits.enabled ? 'ENABLED' : 'DISABLED'} #{limits_values[0]} #{limits_values[1]} #{limits_values[2]} #{limits_values[3]}"
|
|
407
420
|
if limits_values[4] && limits_values[5]
|
|
408
421
|
config << " #{limits_values[4]} #{limits_values[5]}\n"
|
|
409
422
|
else
|
|
410
423
|
config << "\n"
|
|
411
424
|
end
|
|
412
425
|
end
|
|
413
|
-
config <<
|
|
426
|
+
config << @limits.response.to_config if @limits.response
|
|
414
427
|
end
|
|
415
428
|
|
|
416
429
|
if @meta
|
|
@@ -456,19 +469,19 @@ module OpenC3
|
|
|
456
469
|
config['write_conversion'] = self.write_conversion.as_json(*a) if self.write_conversion
|
|
457
470
|
|
|
458
471
|
config['limits'] ||= {}
|
|
459
|
-
if
|
|
472
|
+
if @limits && @limits.enabled
|
|
460
473
|
config['limits']['enabled'] = true
|
|
461
|
-
elsif
|
|
474
|
+
elsif limits_values || (self.state_colors && self.state_colors.length > 0)
|
|
462
475
|
# Only set to false if there are limits or state colors
|
|
463
476
|
# to avoid items without limits acting like they can be enabled
|
|
464
477
|
config['limits']['enabled'] = false
|
|
465
478
|
end
|
|
466
|
-
if
|
|
479
|
+
if limits_values
|
|
467
480
|
# Only set these if there are limits.values because persistence_setting has a default
|
|
468
481
|
# and we don't want keys on the 'limits' hash if there aren't any limits
|
|
469
|
-
config['limits']['persistence_setting'] =
|
|
470
|
-
config['limits']['response'] =
|
|
471
|
-
|
|
482
|
+
config['limits']['persistence_setting'] = @limits.persistence_setting if @limits.persistence_setting
|
|
483
|
+
config['limits']['response'] = @limits.response.to_s if @limits.response
|
|
484
|
+
@limits.values.each do |limits_set, limits_values|
|
|
472
485
|
limits = {}
|
|
473
486
|
limits['red_low'] = limits_values[0]
|
|
474
487
|
limits['yellow_low'] = limits_values[1]
|
|
@@ -26,7 +26,7 @@ module OpenC3
|
|
|
26
26
|
# @param warnings [Array<String>] Array of string warnings which will be
|
|
27
27
|
# appended with any warnings found when parsing the limits
|
|
28
28
|
def self.parse(parser, packet, cmd_or_tlm, item, warnings)
|
|
29
|
-
raise parser.error("Items with LIMITS can't define STATE") if item.
|
|
29
|
+
raise parser.error("Items with LIMITS can't define STATE") if item.limits_values
|
|
30
30
|
raise parser.error("Items with UNITS can't define STATE") if item.units
|
|
31
31
|
|
|
32
32
|
parser = StateParser.new(parser)
|
|
@@ -22,6 +22,10 @@ module OpenC3
|
|
|
22
22
|
class XtceParser
|
|
23
23
|
attr_accessor :current_target_name
|
|
24
24
|
|
|
25
|
+
# The target the file defined, still readable after parsing finishes.
|
|
26
|
+
# current_target_name is cleared by reset_processing_variables.
|
|
27
|
+
attr_reader :parsed_target_name
|
|
28
|
+
|
|
25
29
|
# Processes a XTCE formatted OpenC3 configuration file
|
|
26
30
|
#
|
|
27
31
|
# @param commands [Hash<String=>Packet>] Hash of all the command packets
|
|
@@ -32,8 +36,10 @@ module OpenC3
|
|
|
32
36
|
# that were created while parsing the configuration
|
|
33
37
|
# @param filename [String] The name of the configuration file
|
|
34
38
|
# @param target_name [String] Override the target name found in the XTCE file
|
|
39
|
+
# @return [String] The target name the packets were added under. The caller
|
|
40
|
+
# needs this to build the ID lookup metadata for those packets.
|
|
35
41
|
def self.process(commands, telemetry, warnings, filename, target_name = nil)
|
|
36
|
-
XtceParser.new(commands, telemetry, warnings, filename, target_name)
|
|
42
|
+
XtceParser.new(commands, telemetry, warnings, filename, target_name).parsed_target_name
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
def self.reverse_packet_order(target_name, cmd_or_tlm_hash)
|
|
@@ -91,6 +97,7 @@ module OpenC3
|
|
|
91
97
|
XtceParser.reverse_packet_order(@current_target_name, @commands)
|
|
92
98
|
XtceParser.reverse_packet_order(@current_target_name, @telemetry)
|
|
93
99
|
|
|
100
|
+
@parsed_target_name = @current_target_name
|
|
94
101
|
reset_processing_variables()
|
|
95
102
|
end
|
|
96
103
|
|
|
@@ -143,7 +143,8 @@ module OpenC3
|
|
|
143
143
|
raise ArgumentError, "name must be a String but is a #{name.class}" unless String === name
|
|
144
144
|
raise ArgumentError, "name must contain at least one character" if name.empty?
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
# Dedup since many packets share the same item names
|
|
147
|
+
@name = -name.upcase
|
|
147
148
|
verify_overall() if @structure_item_constructed
|
|
148
149
|
end
|
|
149
150
|
|
|
@@ -151,7 +152,7 @@ module OpenC3
|
|
|
151
152
|
raise ArgumentError, "key must be a String but is a #{key.class}" unless String === key
|
|
152
153
|
raise ArgumentError, "key must contain at least one character" if key.empty?
|
|
153
154
|
|
|
154
|
-
@key = key
|
|
155
|
+
@key = -key
|
|
155
156
|
end
|
|
156
157
|
|
|
157
158
|
def endianness=(endianness)
|
|
@@ -530,7 +530,7 @@ module OpenC3
|
|
|
530
530
|
|
|
531
531
|
value = yield(target_name, packet_name, item_name)
|
|
532
532
|
if comparison_to_eval
|
|
533
|
-
|
|
533
|
+
_check_comparison(target_name, packet_name, item_name, comparison_to_eval, value)
|
|
534
534
|
else
|
|
535
535
|
puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value.nil? ? 'nil' : value.inspect}"
|
|
536
536
|
end
|
|
@@ -733,28 +733,16 @@ module OpenC3
|
|
|
733
733
|
return [target_name, packet_name, item_name, comparison_to_eval, timeout, polling_rate]
|
|
734
734
|
end
|
|
735
735
|
|
|
736
|
-
|
|
736
|
+
# Waits for the comparison to be true or the timeout to expire.
|
|
737
|
+
# The comparison is a callable which takes the telemetry value and returns true or false.
|
|
738
|
+
# A block passed by the user takes precedence over the comparison.
|
|
739
|
+
def _openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: $openc3_scope, token: $openc3_token, &block)
|
|
740
|
+
condition = block || comparison
|
|
737
741
|
end_time = Time.now.sys + timeout
|
|
738
|
-
if exp_to_eval and !exp_to_eval.is_printable?
|
|
739
|
-
raise "ERROR: Invalid comparison to non-ascii value"
|
|
740
|
-
end
|
|
741
742
|
while true
|
|
742
743
|
work_start = Time.now.sys
|
|
743
744
|
value = tlm(target_name, packet_name, item_name, type: value_type, scope: scope, token: token)
|
|
744
|
-
if
|
|
745
|
-
if block.call(value)
|
|
746
|
-
return true, value
|
|
747
|
-
end
|
|
748
|
-
else
|
|
749
|
-
begin
|
|
750
|
-
if eval(exp_to_eval)
|
|
751
|
-
return true, value
|
|
752
|
-
end
|
|
753
|
-
# NoMethodError is raised when the tlm() returns nil and we try to eval the expression
|
|
754
|
-
# In this case we just continue and see if eventually we get a good value from tlm()
|
|
755
|
-
rescue NoMethodError
|
|
756
|
-
end
|
|
757
|
-
end
|
|
745
|
+
return true, value if condition and condition.call(value)
|
|
758
746
|
break if Time.now.sys >= end_time
|
|
759
747
|
|
|
760
748
|
delta = Time.now.sys - work_start
|
|
@@ -766,58 +754,62 @@ module OpenC3
|
|
|
766
754
|
|
|
767
755
|
if canceled
|
|
768
756
|
value = tlm(target_name, packet_name, item_name, type: value_type, scope: scope, token: token)
|
|
769
|
-
if
|
|
770
|
-
|
|
771
|
-
return true, value
|
|
772
|
-
else
|
|
773
|
-
return false, value
|
|
774
|
-
end
|
|
757
|
+
if condition and condition.call(value)
|
|
758
|
+
return true, value
|
|
775
759
|
else
|
|
776
|
-
|
|
777
|
-
if eval(exp_to_eval)
|
|
778
|
-
return true, value
|
|
779
|
-
else
|
|
780
|
-
return false, value
|
|
781
|
-
end
|
|
782
|
-
# NoMethodError is raised when the tlm() returns nil and we try to eval the expression
|
|
783
|
-
rescue NoMethodError
|
|
784
|
-
return false, value
|
|
785
|
-
end
|
|
760
|
+
return false, value
|
|
786
761
|
end
|
|
787
762
|
end
|
|
788
763
|
end
|
|
789
764
|
|
|
790
765
|
return false, value
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# Builds a callable which compares a telemetry value against the given comparison string,
|
|
769
|
+
# e.g. "> 1". Returns nil if there is no comparison, e.g. a block based wait_check().
|
|
770
|
+
# Raises if the comparison is invalid, e.g. an unsupported operator or unparsable operand.
|
|
771
|
+
def _comparison_implementation(comparison_to_eval)
|
|
772
|
+
return nil unless comparison_to_eval
|
|
773
|
+
|
|
774
|
+
operator, operand = extract_operator_and_operand_from_comparison(comparison_to_eval)
|
|
775
|
+
return nil unless operator
|
|
776
|
+
lambda { |value| compare_values(value, operator, operand) }
|
|
799
777
|
end
|
|
800
778
|
|
|
801
779
|
# Wait for a converted telemetry item to pass a comparison
|
|
802
780
|
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)
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
exp_to_eval = nil
|
|
781
|
+
# The comparison text is logged whether or not it is parsed so it is always validated
|
|
782
|
+
if comparison_to_eval and !comparison_to_eval.is_printable?
|
|
783
|
+
raise "ERROR: Invalid comparison to non-ascii value"
|
|
807
784
|
end
|
|
808
|
-
|
|
785
|
+
# A user supplied block is the condition so the comparison text is never parsed
|
|
786
|
+
comparison = block ? nil : _comparison_implementation(comparison_to_eval)
|
|
787
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
809
788
|
end
|
|
810
789
|
|
|
811
790
|
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)
|
|
812
|
-
|
|
813
|
-
|
|
791
|
+
comparison = lambda do |value|
|
|
792
|
+
begin
|
|
793
|
+
value >= (expected_value - tolerance) and value <= (expected_value + tolerance)
|
|
794
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
795
|
+
false
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
814
799
|
end
|
|
815
800
|
|
|
816
801
|
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)
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
802
|
+
comparison = lambda do |values|
|
|
803
|
+
begin
|
|
804
|
+
next false unless values.is_a?(Array) and values.length == array_size
|
|
805
|
+
array_size.times.all? do |i|
|
|
806
|
+
values[i] >= (expected_value[i] - tolerance[i]) and values[i] <= (expected_value[i] + tolerance[i])
|
|
807
|
+
end
|
|
808
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
809
|
+
false
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
821
813
|
end
|
|
822
814
|
|
|
823
815
|
# Wait on an expression to be true.
|
|
@@ -859,17 +851,15 @@ module OpenC3
|
|
|
859
851
|
end
|
|
860
852
|
end
|
|
861
853
|
|
|
862
|
-
def
|
|
863
|
-
string = "value " + comparison_to_eval
|
|
854
|
+
def _check_comparison(target_name, packet_name, item_name, comparison_to_eval, value)
|
|
864
855
|
check_str = "CHECK: #{_upcase(target_name, packet_name, item_name)} #{comparison_to_eval}"
|
|
865
856
|
# Show user the check against a quoted string
|
|
866
|
-
# Note: We have to preserve the original 'value' variable because we're going to eval against it
|
|
867
857
|
value_str = value.is_a?(String) ? "'#{value}'" : value
|
|
868
858
|
value_str = 'nil' if value.nil? # Show user nil value as 'nil'
|
|
869
859
|
with_value = "with value == #{value_str}"
|
|
870
860
|
|
|
871
|
-
|
|
872
|
-
unless
|
|
861
|
+
operator, operand = extract_operator_and_operand_from_comparison(comparison_to_eval)
|
|
862
|
+
unless _valid_comparison?(value, operator, operand)
|
|
873
863
|
message = "Invalid comparison for types"
|
|
874
864
|
if $disconnect
|
|
875
865
|
puts "ERROR: #{message}"
|
|
@@ -878,7 +868,7 @@ module OpenC3
|
|
|
878
868
|
end
|
|
879
869
|
end
|
|
880
870
|
|
|
881
|
-
if
|
|
871
|
+
if compare_values(value, operator, operand)
|
|
882
872
|
puts "#{check_str} success #{with_value}"
|
|
883
873
|
else
|
|
884
874
|
message = "#{check_str} failed #{with_value}"
|
|
@@ -888,35 +878,19 @@ module OpenC3
|
|
|
888
878
|
raise CheckError, message
|
|
889
879
|
end
|
|
890
880
|
end
|
|
891
|
-
rescue NameError => e
|
|
892
|
-
if e.message =~ /uninitialized constant OpenC3::ApiShared::(\w+)/
|
|
893
|
-
new_error = NameError.new("Uninitialized constant #{$1}. Did you mean '#{$1}' as a string?")
|
|
894
|
-
new_error.set_backtrace(e.backtrace)
|
|
895
|
-
raise new_error
|
|
896
|
-
else
|
|
897
|
-
raise e
|
|
898
|
-
end
|
|
899
881
|
end
|
|
900
882
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
operator, operand = extract_operator_and_operand_from_comparison(comparison)
|
|
906
|
-
rescue RuntimeError => e
|
|
907
|
-
if e.message.include?("Unable to parse operand")
|
|
908
|
-
# If we can't parse the operand, let the eval happen anyway
|
|
909
|
-
# It will raise an appropriate error (like NameError for undefined constants)
|
|
910
|
-
return true
|
|
911
|
-
end
|
|
912
|
-
raise # Re-raise invalid operator errors
|
|
913
|
-
rescue JSON::ParserError
|
|
914
|
-
return true
|
|
915
|
-
end
|
|
883
|
+
# Returns whether the value and operand can be meaningfully compared with the operator.
|
|
884
|
+
# Note this is only used by check() because wait() polls until the value changes.
|
|
885
|
+
def _valid_comparison?(value, operator, operand)
|
|
886
|
+
return true if operator.nil?
|
|
916
887
|
|
|
917
888
|
if [">=", "<=", ">", "<"].include?(operator)
|
|
918
|
-
return false if value.nil?
|
|
889
|
+
return false if value.nil? or operand.nil?
|
|
890
|
+
return false if value.is_a?(Array) or operand.is_a?(Array)
|
|
891
|
+
return false if value.is_a?(String) != operand.is_a?(String)
|
|
919
892
|
end
|
|
893
|
+
# Note 'in' does not need a check here because the parser already requires a list operand
|
|
920
894
|
|
|
921
895
|
return true
|
|
922
896
|
end
|
|
@@ -26,6 +26,37 @@ module OpenC3
|
|
|
26
26
|
# whitespace around them is optional.
|
|
27
27
|
SCANNING_REGULAR_EXPRESSION = %r{ (?:"(?:[^\\"]|\\.)*") | (?:'(?:[^\\']|\\.)*') | (?:\[(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*\]) | , | [^\s,]+ }x # "
|
|
28
28
|
|
|
29
|
+
# Operators supported by check(), wait() and wait_check() comparisons
|
|
30
|
+
COMPARISON_OPERATORS = ["==", "!=", ">=", "<=", ">", "<", "in"]
|
|
31
|
+
|
|
32
|
+
# Single character escape sequences processed inside a double quoted comparison operand.
|
|
33
|
+
# The numeric (\nnn, \xHH, \uHHHH, \u{...}) forms are handled by unescape_double_quoted().
|
|
34
|
+
DOUBLE_QUOTE_ESCAPES = {
|
|
35
|
+
'\\' => "\\", '"' => '"', "'" => "'", '#' => '#', 'n' => "\n", 't' => "\t",
|
|
36
|
+
'r' => "\r", 'a' => "\a", 'b' => "\b", 'e' => "\e", 'f' => "\f", 'v' => "\v", 's' => ' '
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Tokenizes a double quoted operand into escape sequences, interpolation markers and runs
|
|
40
|
+
# of plain characters. The alternatives are ordered longest first so \u{1F600} is not read
|
|
41
|
+
# as \u, and the escape alternatives precede the markers so \#{ is a literal '#{'.
|
|
42
|
+
DOUBLE_QUOTE_TOKEN_REGEX =
|
|
43
|
+
/\\u\{[\h\s]*\}|\\u\h{4}|\\x\h{1,2}|\\[0-7]{1,3}|\\M-\\C-.|\\M-.|\\C-.|\\c.|\\.|\#[{@$]|[^\\\#]+|\#/m
|
|
44
|
+
|
|
45
|
+
# Escape sequences which have a meaning we deliberately do not implement. Dropping the
|
|
46
|
+
# backslash would silently change the value so they are rejected instead.
|
|
47
|
+
UNSUPPORTED_ESCAPE_REGEX = /\A\\(?:c|C-|M-)/
|
|
48
|
+
|
|
49
|
+
# The Ruby string interpolation markers. Interpolation would be code execution so it is
|
|
50
|
+
# rejected rather than silently compared against the uninterpolated text.
|
|
51
|
+
INTERPOLATION_REGEX = /\A\#[{@$]/
|
|
52
|
+
|
|
53
|
+
# Limits Ruby enforces on a \u escape. A codepoint is written with at most six hex digits,
|
|
54
|
+
# the largest character Unicode defines is 0x10FFFF, and the surrogate range is reserved
|
|
55
|
+
# for UTF-16 pairs so it is not a character on its own.
|
|
56
|
+
MAX_UNICODE_DIGITS = 6
|
|
57
|
+
MAX_UNICODE_CODEPOINT = 0x10FFFF
|
|
58
|
+
UNICODE_SURROGATE_RANGE = (0xD800..0xDFFF)
|
|
59
|
+
|
|
29
60
|
private
|
|
30
61
|
|
|
31
62
|
# Pulls all string keyword arguments into the args array. Raises on any symbol keyword arguments.
|
|
@@ -165,8 +196,6 @@ module OpenC3
|
|
|
165
196
|
|
|
166
197
|
# Splits `check()` comparison expressions, e.g. "== 'foo bar'" becomes ["==", "foo bar"]
|
|
167
198
|
def extract_operator_and_operand_from_comparison(comparison)
|
|
168
|
-
valid_operators = ["==", "!=", ">=", "<=", ">", "<", "in"]
|
|
169
|
-
|
|
170
199
|
operator, operand = comparison.split(nil, 2) # Ruby: second split arg is max number of resultant elements
|
|
171
200
|
|
|
172
201
|
if operand.nil?
|
|
@@ -175,29 +204,196 @@ module OpenC3
|
|
|
175
204
|
return [nil, nil]
|
|
176
205
|
end
|
|
177
206
|
|
|
178
|
-
raise "ERROR: Invalid operator: '#{operator}'" unless
|
|
207
|
+
raise "ERROR: Invalid operator: '#{operator}'" unless COMPARISON_OPERATORS.include?(operator)
|
|
208
|
+
|
|
209
|
+
# split leaves any trailing whitespace on the operand which the anchored matches reject
|
|
210
|
+
operand = extract_operand(operand.strip)
|
|
211
|
+
# 'in' is containment against a list of values in both Ruby and Python.
|
|
212
|
+
# Enforced here so check(), wait() and wait_check() all reject the same thing.
|
|
213
|
+
if operator == "in" and !operand.is_a?(Array)
|
|
214
|
+
raise "ERROR: The 'in' operator requires a list operand: #{operand.inspect}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
return [operator, operand]
|
|
218
|
+
end
|
|
179
219
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
220
|
+
# Converts the operand of a `check()` comparison expression into a Ruby value.
|
|
221
|
+
# Note this deliberately does not eval the operand so only literal values are supported.
|
|
222
|
+
def extract_operand(operand)
|
|
223
|
+
# A quoted operand must be a single complete string literal. Anything trailing the
|
|
224
|
+
# closing quote, e.g. "== 'a' garbage 'b'", is a syntax error rather than a string.
|
|
225
|
+
if (match = operand.match(/\A'((?:[^'\\]|\\.)*)'\z/m))
|
|
226
|
+
# Ruby single quoted strings only escape the backslash and the single quote
|
|
227
|
+
return match[1].gsub(/\\([\\'])/) { $1 }
|
|
228
|
+
end
|
|
229
|
+
if (match = operand.match(/\A"((?:[^"\\]|\\.)*)"\z/m))
|
|
230
|
+
return unescape_double_quoted(match[1])
|
|
184
231
|
end
|
|
232
|
+
return nil if operand == "nil"
|
|
233
|
+
return false if operand == "false"
|
|
234
|
+
return true if operand == "true"
|
|
185
235
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
# Ruby's Float() rejects these but Python's float() accepts them
|
|
237
|
+
case operand.upcase
|
|
238
|
+
when 'INFINITY', '+INFINITY', 'INF', '+INF'
|
|
239
|
+
return Float::INFINITY
|
|
240
|
+
when '-INFINITY', '-INF'
|
|
241
|
+
return -Float::INFINITY
|
|
242
|
+
when 'NAN', '+NAN', '-NAN'
|
|
243
|
+
return Float::NAN
|
|
193
244
|
else
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
245
|
+
# Not an infinity or NaN keyword so fall through to the formats below
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Arrays are parsed recursively so their elements follow exactly the same rules as a
|
|
249
|
+
# bare operand. JSON alone would reject ['ON', 'OFF'], [0xA, 0xB] and [true, nil].
|
|
250
|
+
if operand.start_with?('[') and operand.end_with?(']')
|
|
251
|
+
return split_operand_list(operand[1..-2]).map { |element| extract_operand(element) }
|
|
252
|
+
end
|
|
253
|
+
# JSON handles decimal numbers and hashes
|
|
254
|
+
begin
|
|
255
|
+
return JSON.parse(operand)
|
|
256
|
+
rescue JSON::ParserError
|
|
257
|
+
# Fall through to the number formats JSON does not accept
|
|
258
|
+
end
|
|
259
|
+
# Number formats JSON does not accept
|
|
260
|
+
begin
|
|
261
|
+
# Explicit base prefix: hex (0x), octal (0o) or binary (0b)
|
|
262
|
+
return Integer(operand) if operand.match?(/\A[+-]?0[xXoObB][0-9a-fA-F_]+\z/)
|
|
263
|
+
# Integers with underscore separators, e.g. 1_000. A leading zero is deliberately
|
|
264
|
+
# rejected because it is ambiguous: Ruby reads 010 as octal 8 while Python rejects it.
|
|
265
|
+
# Write 0o10 for octal 8 or 10 for decimal 10.
|
|
266
|
+
return Integer(operand, 10) if operand.match?(/\A[+-]?[1-9][0-9_]*\z/)
|
|
267
|
+
# Floats with underscore separators, e.g. 1_000.5. A leading zero is unambiguous here.
|
|
268
|
+
return Float(operand) if operand.match?(/\A[+-]?[0-9][0-9_]*(?:\.[0-9_]+(?:[eE][+-]?[0-9_]+)?|[eE][+-]?[0-9_]+)\z/)
|
|
269
|
+
rescue ArgumentError
|
|
270
|
+
# Fall through to the error cases
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# A bare word is almost always a string the user forgot to quote
|
|
274
|
+
if operand.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
275
|
+
raise NameError, "Uninitialized constant #{operand}. Did you mean '#{operand}' as a string?"
|
|
276
|
+
end
|
|
277
|
+
raise "ERROR: Unable to parse operand: #{operand}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Processes the escape sequences in the contents of a double quoted operand
|
|
281
|
+
def unescape_double_quoted(string)
|
|
282
|
+
# A \xHH or octal escape can produce a byte which is not valid UTF-8 so build the result
|
|
283
|
+
# in binary. Otherwise preserve the encoding of the comparison the user passed in.
|
|
284
|
+
# The escape must not itself be escaped, e.g. "C:\\0" is a backslash followed by a zero.
|
|
285
|
+
binary = string.match?(/(?<!\\)(?:\\\\)*\\(?:x\h|[0-7])/)
|
|
286
|
+
result = binary ? ''.b : String.new(encoding: string.encoding)
|
|
287
|
+
string.scan(DOUBLE_QUOTE_TOKEN_REGEX).each do |token|
|
|
288
|
+
replacement =
|
|
289
|
+
if token.match?(INTERPOLATION_REGEX)
|
|
290
|
+
raise "ERROR: String interpolation is not supported in an operand: #{token}. " +
|
|
291
|
+
'Interpolate in the script itself or escape it as \#' + token[1] +
|
|
292
|
+
' to compare against the literal text'
|
|
293
|
+
elsif !token.start_with?('\\')
|
|
294
|
+
token
|
|
295
|
+
elsif token.start_with?('\u{')
|
|
296
|
+
# \u{1F600} or \u{48 49} which is one or more whitespace separated codepoints
|
|
297
|
+
token[3..-2].split.map { |codepoint| pack_unicode_codepoint(codepoint, token) }.join
|
|
298
|
+
elsif token.length == 6 and token.start_with?('\u')
|
|
299
|
+
pack_unicode_codepoint(token[2..-1], token)
|
|
300
|
+
elsif token.length > 2 and token.start_with?('\x')
|
|
301
|
+
[token[2..-1].hex].pack('C')
|
|
302
|
+
elsif token.match?(/\A\\[0-7]/)
|
|
303
|
+
[token[1..-1].to_i(8) & 0xFF].pack('C')
|
|
304
|
+
elsif token.match?(UNSUPPORTED_ESCAPE_REGEX)
|
|
305
|
+
raise "ERROR: Unsupported escape sequence in operand: #{token}"
|
|
306
|
+
elsif token == '\u' or token == '\x'
|
|
307
|
+
raise "ERROR: Invalid escape sequence in operand: #{token}"
|
|
308
|
+
else
|
|
309
|
+
# Ruby drops the backslash of an escape which has no special meaning, e.g. "\q"
|
|
310
|
+
DOUBLE_QUOTE_ESCAPES.fetch(token[1], token[1])
|
|
311
|
+
end
|
|
312
|
+
result << (binary ? replacement.b : replacement)
|
|
313
|
+
end
|
|
314
|
+
return result
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Packs a single codepoint of a \u escape into a UTF-8 character. Ruby rejects a codepoint
|
|
318
|
+
# which is too long, above the largest Unicode character or in the UTF-16 surrogate range,
|
|
319
|
+
# so those raise here rather than silently packing into an invalid UTF-8 string.
|
|
320
|
+
def pack_unicode_codepoint(codepoint, token)
|
|
321
|
+
value = codepoint.hex
|
|
322
|
+
if codepoint.length > MAX_UNICODE_DIGITS or value > MAX_UNICODE_CODEPOINT or
|
|
323
|
+
UNICODE_SURROGATE_RANGE.include?(value)
|
|
324
|
+
raise "ERROR: Invalid Unicode codepoint in operand: #{token}"
|
|
325
|
+
end
|
|
326
|
+
return [value].pack('U')
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Splits the contents of a bracketed operand list on the top level commas.
|
|
330
|
+
# Quotes and nesting are tracked so "['a,b', [1, 2]]" is two elements, not four.
|
|
331
|
+
def split_operand_list(text)
|
|
332
|
+
elements = []
|
|
333
|
+
current = String.new(encoding: text.encoding)
|
|
334
|
+
depth = 0
|
|
335
|
+
quote = nil
|
|
336
|
+
escaped = false
|
|
337
|
+
text.each_char do |char|
|
|
338
|
+
if escaped
|
|
339
|
+
escaped = false
|
|
340
|
+
elsif quote
|
|
341
|
+
if char == '\\'
|
|
342
|
+
escaped = true
|
|
343
|
+
elsif char == quote
|
|
344
|
+
quote = nil
|
|
345
|
+
end
|
|
346
|
+
elsif char == "'" or char == '"'
|
|
347
|
+
quote = char
|
|
348
|
+
elsif char == '[' or char == '{'
|
|
349
|
+
depth += 1
|
|
350
|
+
elsif char == ']' or char == '}'
|
|
351
|
+
depth -= 1
|
|
352
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if depth < 0
|
|
353
|
+
elsif char == ',' and depth == 0
|
|
354
|
+
elements << current.strip
|
|
355
|
+
current = String.new(encoding: text.encoding)
|
|
356
|
+
next
|
|
198
357
|
end
|
|
358
|
+
current << char
|
|
199
359
|
end
|
|
200
|
-
|
|
360
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if depth != 0 or quote
|
|
361
|
+
|
|
362
|
+
elements << current.strip
|
|
363
|
+
# A single empty element is an empty list, e.g. []
|
|
364
|
+
return [] if elements.length == 1 and elements[0].empty?
|
|
365
|
+
# A trailing comma is allowed, e.g. [1,], matching both Ruby and Python list literals
|
|
366
|
+
elements.pop if elements[-1].empty?
|
|
367
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if elements.empty? or elements.any?(&:empty?)
|
|
368
|
+
|
|
369
|
+
return elements
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Compares a telemetry value against an operand using the given operator.
|
|
373
|
+
# Returns false rather than raising if the two values can not be compared.
|
|
374
|
+
def compare_values(value, operator, operand)
|
|
375
|
+
case operator
|
|
376
|
+
when "=="
|
|
377
|
+
value == operand
|
|
378
|
+
when "!="
|
|
379
|
+
value != operand
|
|
380
|
+
when ">"
|
|
381
|
+
value > operand
|
|
382
|
+
when ">="
|
|
383
|
+
value >= operand
|
|
384
|
+
when "<"
|
|
385
|
+
value < operand
|
|
386
|
+
when "<="
|
|
387
|
+
value <= operand
|
|
388
|
+
when "in"
|
|
389
|
+
# 'in' is containment against a list of values, matching Python
|
|
390
|
+
operand.is_a?(Array) && operand.include?(value)
|
|
391
|
+
else
|
|
392
|
+
raise "ERROR: Invalid operator: '#{operator}'"
|
|
393
|
+
end
|
|
394
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
395
|
+
# Comparing incompatible types, e.g. nil > 1, is simply not a match
|
|
396
|
+
false
|
|
201
397
|
end
|
|
202
398
|
end
|
|
203
399
|
end
|