openc3 7.4.1 → 7.5.1
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/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 +149 -160
- 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 +225 -71
- 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/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/logs/log_writer.rb +14 -4
- data/lib/openc3/microservices/decom_microservice.rb +30 -5
- data/lib/openc3/microservices/interface_decom_common.rb +3 -1
- 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/cvt_model.rb +7 -2
- 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/target_model.rb +92 -15
- 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 +12 -1
- data/lib/openc3/packets/packet_item.rb +40 -22
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/structure_item.rb +3 -2
- data/lib/openc3/script/api_shared.rb +62 -86
- data/lib/openc3/script/extract.rb +263 -20
- data/lib/openc3/script/plugins.rb +4 -11
- data/lib/openc3/script/script.rb +1 -1
- data/lib/openc3/script/storage.rb +81 -10
- 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/limits_event_topic.rb +90 -3
- data/lib/openc3/topics/router_topic.rb +7 -2
- data/lib/openc3/topics/system_events_topic.rb +9 -6
- 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 +162 -3
- data/lib/openc3/utilities/bucket.rb +5 -0
- 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 +140 -32
- data/lib/openc3/utilities/running_script.rb +22 -18
- data/lib/openc3/utilities/secrets.rb +5 -0
- data/lib/openc3/utilities/store_autoload.rb +23 -4
- 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 +16 -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)
|
|
@@ -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)
|
|
@@ -37,7 +37,7 @@ module OpenC3
|
|
|
37
37
|
# @param args [String|Array<String>] See the description for calling style
|
|
38
38
|
# @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED
|
|
39
39
|
def check(*args, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token)
|
|
40
|
-
_check(*args, scope: scope) { |tgt, pkt, item| tlm(tgt, pkt, item, type: type, scope: scope, token: token) }
|
|
40
|
+
_check(*args, type: type, scope: scope, token: token) { |tgt, pkt, item| tlm(tgt, pkt, item, type: type, scope: scope, token: token) }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def check_raw(*args, scope: $openc3_scope, token: $openc3_token)
|
|
@@ -525,12 +525,12 @@ module OpenC3
|
|
|
525
525
|
# Implementation of the various check commands. It yields back to the
|
|
526
526
|
# caller to allow the return of the value through various telemetry calls.
|
|
527
527
|
# This method should not be called directly by application code.
|
|
528
|
-
def _check(*args, scope: $openc3_scope, token: $openc3_token)
|
|
528
|
+
def _check(*args, type: :CONVERTED, scope: $openc3_scope, token: $openc3_token)
|
|
529
529
|
target_name, packet_name, item_name, comparison_to_eval = _check_process_args(args, 'check')
|
|
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, type, comparison_to_eval, value, scope: scope, token: token)
|
|
534
534
|
else
|
|
535
535
|
puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value.nil? ? 'nil' : value.inspect}"
|
|
536
536
|
end
|
|
@@ -596,6 +596,8 @@ module OpenC3
|
|
|
596
596
|
quiet: false,
|
|
597
597
|
scope: $openc3_scope, token: $openc3_token)
|
|
598
598
|
type = (check ? 'CHECK' : 'WAIT')
|
|
599
|
+
# LATEST would compare the RECEIVED_COUNT of whichever packet was received most recently
|
|
600
|
+
raise "LATEST is not supported by wait_packet, specify a packet name" if packet_name.to_s.upcase == 'LATEST'
|
|
599
601
|
initial_count = tlm(target_name, packet_name, 'RECEIVED_COUNT', scope: scope, token: token)
|
|
600
602
|
# If the packet has not been received the initial_count could be nil
|
|
601
603
|
initial_count = 0 unless initial_count
|
|
@@ -733,28 +735,16 @@ module OpenC3
|
|
|
733
735
|
return [target_name, packet_name, item_name, comparison_to_eval, timeout, polling_rate]
|
|
734
736
|
end
|
|
735
737
|
|
|
736
|
-
|
|
738
|
+
# Waits for the comparison to be true or the timeout to expire.
|
|
739
|
+
# The comparison is a callable which takes the telemetry value and returns true or false.
|
|
740
|
+
# A block passed by the user takes precedence over the comparison.
|
|
741
|
+
def _openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: $openc3_scope, token: $openc3_token, &block)
|
|
742
|
+
condition = block || comparison
|
|
737
743
|
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
744
|
while true
|
|
742
745
|
work_start = Time.now.sys
|
|
743
746
|
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
|
|
747
|
+
return true, value if condition and condition.call(value)
|
|
758
748
|
break if Time.now.sys >= end_time
|
|
759
749
|
|
|
760
750
|
delta = Time.now.sys - work_start
|
|
@@ -766,58 +756,62 @@ module OpenC3
|
|
|
766
756
|
|
|
767
757
|
if canceled
|
|
768
758
|
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
|
|
759
|
+
if condition and condition.call(value)
|
|
760
|
+
return true, value
|
|
775
761
|
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
|
|
762
|
+
return false, value
|
|
786
763
|
end
|
|
787
764
|
end
|
|
788
765
|
end
|
|
789
766
|
|
|
790
767
|
return false, value
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# Builds a callable which compares a telemetry value against the given comparison string,
|
|
771
|
+
# e.g. "> 1". Returns nil if there is no comparison, e.g. a block based wait_check().
|
|
772
|
+
# Raises if the comparison is invalid, e.g. an unsupported operator or unparsable operand.
|
|
773
|
+
def _comparison_implementation(target_name, packet_name, item_name, value_type, comparison_to_eval, scope: $openc3_scope, token: $openc3_token)
|
|
774
|
+
return nil unless comparison_to_eval
|
|
775
|
+
|
|
776
|
+
operator, operand = extract_comparison(comparison_to_eval, value_type) { get_item(target_name, packet_name, item_name, scope: scope, token: token) }
|
|
777
|
+
return nil unless operator
|
|
778
|
+
lambda { |value| compare_values(value, operator, operand) }
|
|
799
779
|
end
|
|
800
780
|
|
|
801
781
|
# Wait for a converted telemetry item to pass a comparison
|
|
802
782
|
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
|
|
783
|
+
# The comparison text is logged whether or not it is parsed so it is always validated
|
|
784
|
+
if comparison_to_eval and !comparison_to_eval.is_printable?
|
|
785
|
+
raise "ERROR: Invalid comparison to non-ascii value"
|
|
807
786
|
end
|
|
808
|
-
|
|
787
|
+
# A user supplied block is the condition so the comparison text is never parsed
|
|
788
|
+
comparison = block ? nil : _comparison_implementation(target_name, packet_name, item_name, value_type, comparison_to_eval, scope: scope, token: token)
|
|
789
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
809
790
|
end
|
|
810
791
|
|
|
811
792
|
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
|
-
|
|
793
|
+
comparison = lambda do |value|
|
|
794
|
+
begin
|
|
795
|
+
value >= (expected_value - tolerance) and value <= (expected_value + tolerance)
|
|
796
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
797
|
+
false
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
814
801
|
end
|
|
815
802
|
|
|
816
803
|
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
|
-
|
|
804
|
+
comparison = lambda do |values|
|
|
805
|
+
begin
|
|
806
|
+
next false unless values.is_a?(Array) and values.length == array_size
|
|
807
|
+
array_size.times.all? do |i|
|
|
808
|
+
values[i] >= (expected_value[i] - tolerance[i]) and values[i] <= (expected_value[i] + tolerance[i])
|
|
809
|
+
end
|
|
810
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
811
|
+
false
|
|
812
|
+
end
|
|
813
|
+
end
|
|
814
|
+
_openc3_script_wait_implementation(target_name, packet_name, item_name, value_type, timeout, polling_rate, comparison, scope: scope, token: token, &block)
|
|
821
815
|
end
|
|
822
816
|
|
|
823
817
|
# Wait on an expression to be true.
|
|
@@ -859,17 +853,15 @@ module OpenC3
|
|
|
859
853
|
end
|
|
860
854
|
end
|
|
861
855
|
|
|
862
|
-
def
|
|
863
|
-
string = "value " + comparison_to_eval
|
|
856
|
+
def _check_comparison(target_name, packet_name, item_name, value_type, comparison_to_eval, value, scope: $openc3_scope, token: $openc3_token)
|
|
864
857
|
check_str = "CHECK: #{_upcase(target_name, packet_name, item_name)} #{comparison_to_eval}"
|
|
865
858
|
# 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
859
|
value_str = value.is_a?(String) ? "'#{value}'" : value
|
|
868
860
|
value_str = 'nil' if value.nil? # Show user nil value as 'nil'
|
|
869
861
|
with_value = "with value == #{value_str}"
|
|
870
862
|
|
|
871
|
-
|
|
872
|
-
unless
|
|
863
|
+
operator, operand = extract_comparison(comparison_to_eval, value_type) { get_item(target_name, packet_name, item_name, scope: scope, token: token) }
|
|
864
|
+
unless _valid_comparison?(value, operator, operand)
|
|
873
865
|
message = "Invalid comparison for types"
|
|
874
866
|
if $disconnect
|
|
875
867
|
puts "ERROR: #{message}"
|
|
@@ -878,7 +870,7 @@ module OpenC3
|
|
|
878
870
|
end
|
|
879
871
|
end
|
|
880
872
|
|
|
881
|
-
if
|
|
873
|
+
if compare_values(value, operator, operand)
|
|
882
874
|
puts "#{check_str} success #{with_value}"
|
|
883
875
|
else
|
|
884
876
|
message = "#{check_str} failed #{with_value}"
|
|
@@ -888,35 +880,19 @@ module OpenC3
|
|
|
888
880
|
raise CheckError, message
|
|
889
881
|
end
|
|
890
882
|
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
883
|
end
|
|
900
884
|
|
|
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
|
|
885
|
+
# Returns whether the value and operand can be meaningfully compared with the operator.
|
|
886
|
+
# Note this is only used by check() because wait() polls until the value changes.
|
|
887
|
+
def _valid_comparison?(value, operator, operand)
|
|
888
|
+
return true if operator.nil?
|
|
916
889
|
|
|
917
890
|
if [">=", "<=", ">", "<"].include?(operator)
|
|
918
|
-
return false if value.nil?
|
|
891
|
+
return false if value.nil? or operand.nil?
|
|
892
|
+
return false if value.is_a?(Array) or operand.is_a?(Array)
|
|
893
|
+
return false if value.is_a?(String) != operand.is_a?(String)
|
|
919
894
|
end
|
|
895
|
+
# Note 'in' does not need a check here because the parser already requires a list operand
|
|
920
896
|
|
|
921
897
|
return true
|
|
922
898
|
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.
|
|
@@ -164,9 +195,8 @@ module OpenC3
|
|
|
164
195
|
end
|
|
165
196
|
|
|
166
197
|
# Splits `check()` comparison expressions, e.g. "== 'foo bar'" becomes ["==", "foo bar"]
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
# The optional block resolves an unquoted word, see extract_operand
|
|
199
|
+
def extract_operator_and_operand_from_comparison(comparison, &resolve_name)
|
|
170
200
|
operator, operand = comparison.split(nil, 2) # Ruby: second split arg is max number of resultant elements
|
|
171
201
|
|
|
172
202
|
if operand.nil?
|
|
@@ -175,29 +205,242 @@ module OpenC3
|
|
|
175
205
|
return [nil, nil]
|
|
176
206
|
end
|
|
177
207
|
|
|
178
|
-
raise "ERROR: Invalid operator: '#{operator}'" unless
|
|
208
|
+
raise "ERROR: Invalid operator: '#{operator}'" unless COMPARISON_OPERATORS.include?(operator)
|
|
179
209
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
210
|
+
# split leaves any trailing whitespace on the operand which the anchored matches reject
|
|
211
|
+
operand = extract_operand(operand.strip, &resolve_name)
|
|
212
|
+
# 'in' is containment against a list of values in both Ruby and Python.
|
|
213
|
+
# Enforced here so check(), wait() and wait_check() all reject the same thing.
|
|
214
|
+
if operator == "in" and !operand.is_a?(Array)
|
|
215
|
+
raise "ERROR: The 'in' operator requires a list operand: #{operand.inspect}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
return [operator, operand]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Converts the operand of a `check()` comparison expression into a Ruby value.
|
|
222
|
+
# Note this deliberately does not eval the operand so only literal values are supported.
|
|
223
|
+
# The optional block is called with an unquoted word, including one inside a list, and
|
|
224
|
+
# returns the value to use or nil if the word is not recognized, e.g. a state name.
|
|
225
|
+
def extract_operand(operand, &resolve_name)
|
|
226
|
+
# A quoted operand must be a single complete string literal. Anything trailing the
|
|
227
|
+
# closing quote, e.g. "== 'a' garbage 'b'", is a syntax error rather than a string.
|
|
228
|
+
if (match = operand.match(/\A'((?:[^'\\]|\\.)*)'\z/m))
|
|
229
|
+
# Ruby single quoted strings only escape the backslash and the single quote
|
|
230
|
+
return match[1].gsub(/\\([\\'])/) { $1 }
|
|
231
|
+
end
|
|
232
|
+
if (match = operand.match(/\A"((?:[^"\\]|\\.)*)"\z/m))
|
|
233
|
+
return unescape_double_quoted(match[1])
|
|
184
234
|
end
|
|
235
|
+
return nil if operand == "nil"
|
|
236
|
+
return false if operand == "false"
|
|
237
|
+
return true if operand == "true"
|
|
185
238
|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
239
|
+
# Ruby's Float() rejects these but Python's float() accepts them
|
|
240
|
+
case operand.upcase
|
|
241
|
+
when 'INFINITY', '+INFINITY', 'INF', '+INF'
|
|
242
|
+
return Float::INFINITY
|
|
243
|
+
when '-INFINITY', '-INF'
|
|
244
|
+
return -Float::INFINITY
|
|
245
|
+
when 'NAN', '+NAN', '-NAN'
|
|
246
|
+
return Float::NAN
|
|
193
247
|
else
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
248
|
+
# Not an infinity or NaN keyword so fall through to the formats below
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Arrays are parsed recursively so their elements follow exactly the same rules as a
|
|
252
|
+
# bare operand. JSON alone would reject ['ON', 'OFF'], [0xA, 0xB] and [true, nil].
|
|
253
|
+
if operand.start_with?('[') and operand.end_with?(']')
|
|
254
|
+
return split_operand_list(operand[1..-2]).map { |element| extract_operand(element, &resolve_name) }
|
|
255
|
+
end
|
|
256
|
+
# JSON handles decimal numbers and hashes
|
|
257
|
+
begin
|
|
258
|
+
return JSON.parse(operand)
|
|
259
|
+
rescue JSON::ParserError
|
|
260
|
+
# Fall through to the number formats JSON does not accept
|
|
261
|
+
end
|
|
262
|
+
# Number formats JSON does not accept
|
|
263
|
+
begin
|
|
264
|
+
# Explicit base prefix: hex (0x), octal (0o) or binary (0b)
|
|
265
|
+
return Integer(operand) if operand.match?(/\A[+-]?0[xXoObB][0-9a-fA-F_]+\z/)
|
|
266
|
+
# Integers with underscore separators, e.g. 1_000. A leading zero is deliberately
|
|
267
|
+
# rejected because it is ambiguous: Ruby reads 010 as octal 8 while Python rejects it.
|
|
268
|
+
# Write 0o10 for octal 8 or 10 for decimal 10.
|
|
269
|
+
return Integer(operand, 10) if operand.match?(/\A[+-]?[1-9][0-9_]*\z/)
|
|
270
|
+
# Floats with underscore separators, e.g. 1_000.5. A leading zero is unambiguous here.
|
|
271
|
+
return Float(operand) if operand.match?(/\A[+-]?[0-9][0-9_]*(?:\.[0-9_]+(?:[eE][+-]?[0-9_]+)?|[eE][+-]?[0-9_]+)\z/)
|
|
272
|
+
rescue ArgumentError
|
|
273
|
+
# Fall through to the error cases
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# A bare word is almost always a string the user forgot to quote
|
|
277
|
+
if operand.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
278
|
+
value = resolve_name&.call(operand)
|
|
279
|
+
return value unless value.nil?
|
|
280
|
+
|
|
281
|
+
raise NameError, "Unknown state name #{operand}. Did you mean '#{operand}' as a string?"
|
|
282
|
+
end
|
|
283
|
+
raise "ERROR: Unable to parse operand: #{operand}"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Processes the escape sequences in the contents of a double quoted operand
|
|
287
|
+
def unescape_double_quoted(string)
|
|
288
|
+
# A \xHH or octal escape can produce a byte which is not valid UTF-8 so build the result
|
|
289
|
+
# in binary. Otherwise preserve the encoding of the comparison the user passed in.
|
|
290
|
+
# The escape must not itself be escaped, e.g. "C:\\0" is a backslash followed by a zero.
|
|
291
|
+
binary = string.match?(/(?<!\\)(?:\\\\)*\\(?:x\h|[0-7])/)
|
|
292
|
+
result = binary ? ''.b : String.new(encoding: string.encoding)
|
|
293
|
+
string.scan(DOUBLE_QUOTE_TOKEN_REGEX).each do |token|
|
|
294
|
+
replacement =
|
|
295
|
+
if token.match?(INTERPOLATION_REGEX)
|
|
296
|
+
raise "ERROR: String interpolation is not supported in an operand: #{token}. " +
|
|
297
|
+
'Interpolate in the script itself or escape it as \#' + token[1] +
|
|
298
|
+
' to compare against the literal text'
|
|
299
|
+
elsif !token.start_with?('\\')
|
|
300
|
+
token
|
|
301
|
+
elsif token.start_with?('\u{')
|
|
302
|
+
# \u{1F600} or \u{48 49} which is one or more whitespace separated codepoints
|
|
303
|
+
token[3..-2].split.map { |codepoint| pack_unicode_codepoint(codepoint, token) }.join
|
|
304
|
+
elsif token.length == 6 and token.start_with?('\u')
|
|
305
|
+
pack_unicode_codepoint(token[2..-1], token)
|
|
306
|
+
elsif token.length > 2 and token.start_with?('\x')
|
|
307
|
+
[token[2..-1].hex].pack('C')
|
|
308
|
+
elsif token.match?(/\A\\[0-7]/)
|
|
309
|
+
[token[1..-1].to_i(8) & 0xFF].pack('C')
|
|
310
|
+
elsif token.match?(UNSUPPORTED_ESCAPE_REGEX)
|
|
311
|
+
raise "ERROR: Unsupported escape sequence in operand: #{token}"
|
|
312
|
+
elsif token == '\u' or token == '\x'
|
|
313
|
+
raise "ERROR: Invalid escape sequence in operand: #{token}"
|
|
314
|
+
else
|
|
315
|
+
# Ruby drops the backslash of an escape which has no special meaning, e.g. "\q"
|
|
316
|
+
DOUBLE_QUOTE_ESCAPES.fetch(token[1], token[1])
|
|
317
|
+
end
|
|
318
|
+
result << (binary ? replacement.b : replacement)
|
|
319
|
+
end
|
|
320
|
+
return result
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Packs a single codepoint of a \u escape into a UTF-8 character. Ruby rejects a codepoint
|
|
324
|
+
# which is too long, above the largest Unicode character or in the UTF-16 surrogate range,
|
|
325
|
+
# so those raise here rather than silently packing into an invalid UTF-8 string.
|
|
326
|
+
def pack_unicode_codepoint(codepoint, token)
|
|
327
|
+
value = codepoint.hex
|
|
328
|
+
if codepoint.length > MAX_UNICODE_DIGITS or value > MAX_UNICODE_CODEPOINT or
|
|
329
|
+
UNICODE_SURROGATE_RANGE.include?(value)
|
|
330
|
+
raise "ERROR: Invalid Unicode codepoint in operand: #{token}"
|
|
331
|
+
end
|
|
332
|
+
return [value].pack('U')
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Splits the contents of a bracketed operand list on the top level commas.
|
|
336
|
+
# Quotes and nesting are tracked so "['a,b', [1, 2]]" is two elements, not four.
|
|
337
|
+
def split_operand_list(text)
|
|
338
|
+
elements = []
|
|
339
|
+
current = String.new(encoding: text.encoding)
|
|
340
|
+
depth = 0
|
|
341
|
+
quote = nil
|
|
342
|
+
escaped = false
|
|
343
|
+
text.each_char do |char|
|
|
344
|
+
if escaped
|
|
345
|
+
escaped = false
|
|
346
|
+
elsif quote
|
|
347
|
+
if char == '\\'
|
|
348
|
+
escaped = true
|
|
349
|
+
elsif char == quote
|
|
350
|
+
quote = nil
|
|
351
|
+
end
|
|
352
|
+
elsif char == "'" or char == '"'
|
|
353
|
+
quote = char
|
|
354
|
+
elsif char == '[' or char == '{'
|
|
355
|
+
depth += 1
|
|
356
|
+
elsif char == ']' or char == '}'
|
|
357
|
+
depth -= 1
|
|
358
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if depth < 0
|
|
359
|
+
elsif char == ',' and depth == 0
|
|
360
|
+
elements << current.strip
|
|
361
|
+
current = String.new(encoding: text.encoding)
|
|
362
|
+
next
|
|
198
363
|
end
|
|
364
|
+
current << char
|
|
199
365
|
end
|
|
200
|
-
|
|
366
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if depth != 0 or quote
|
|
367
|
+
|
|
368
|
+
elements << current.strip
|
|
369
|
+
# A single empty element is an empty list, e.g. []
|
|
370
|
+
return [] if elements.length == 1 and elements[0].empty?
|
|
371
|
+
# A trailing comma is allowed, e.g. [1,], matching both Ruby and Python list literals
|
|
372
|
+
elements.pop if elements[-1].empty?
|
|
373
|
+
raise "ERROR: Unable to parse operand: [#{text}]" if elements.empty? or elements.any?(&:empty?)
|
|
374
|
+
|
|
375
|
+
return elements
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Splits a comparison like extract_operator_and_operand_from_comparison but matches state names.
|
|
379
|
+
# State names are stored upper case so a string operand which matches a state name ignoring case
|
|
380
|
+
# is compared against that state name, e.g. "== 'Connected'" matches the CONNECTED state.
|
|
381
|
+
# An unquoted state name, e.g. "== Connected" or "in [On, Off]", is also accepted. The block returns
|
|
382
|
+
# the item hash. It is only called once, when the operand could be a state name, so numeric
|
|
383
|
+
# comparisons don't make an additional API call. RAW values are never state names so it is never called.
|
|
384
|
+
def extract_comparison(comparison, value_type, &get_item)
|
|
385
|
+
item_states = nil
|
|
386
|
+
looked_up = false
|
|
387
|
+
states = lambda do
|
|
388
|
+
unless looked_up
|
|
389
|
+
looked_up = true
|
|
390
|
+
item_states = get_item.call['states'] unless value_type.to_s.upcase == 'RAW'
|
|
391
|
+
end
|
|
392
|
+
item_states
|
|
393
|
+
end
|
|
394
|
+
operator, operand = extract_operator_and_operand_from_comparison(comparison) do |name|
|
|
395
|
+
name.upcase if states.call&.key?(name.upcase)
|
|
396
|
+
end
|
|
397
|
+
return [operator, operand] unless operand_contains_string?(operand)
|
|
398
|
+
|
|
399
|
+
return [operator, states.call ? upcase_state_names(operand, states.call) : operand]
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Returns whether the operand is or contains a string, i.e. could be a state name
|
|
403
|
+
def operand_contains_string?(operand)
|
|
404
|
+
return true if operand.is_a?(String)
|
|
405
|
+
return operand.any? { |element| operand_contains_string?(element) } if operand.is_a?(Array)
|
|
406
|
+
return false
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# State names are stored upper case so any string in the operand which matches
|
|
410
|
+
# a state name ignoring case is replaced with that state name
|
|
411
|
+
def upcase_state_names(operand, states)
|
|
412
|
+
if operand.is_a?(String)
|
|
413
|
+
return states.key?(operand.upcase) ? operand.upcase : operand
|
|
414
|
+
end
|
|
415
|
+
return operand.map { |element| upcase_state_names(element, states) } if operand.is_a?(Array)
|
|
416
|
+
return operand
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Compares a telemetry value against an operand using the given operator.
|
|
420
|
+
# Returns false rather than raising if the two values can not be compared.
|
|
421
|
+
def compare_values(value, operator, operand)
|
|
422
|
+
case operator
|
|
423
|
+
when "=="
|
|
424
|
+
value == operand
|
|
425
|
+
when "!="
|
|
426
|
+
value != operand
|
|
427
|
+
when ">"
|
|
428
|
+
value > operand
|
|
429
|
+
when ">="
|
|
430
|
+
value >= operand
|
|
431
|
+
when "<"
|
|
432
|
+
value < operand
|
|
433
|
+
when "<="
|
|
434
|
+
value <= operand
|
|
435
|
+
when "in"
|
|
436
|
+
# 'in' is containment against a list of values, matching Python
|
|
437
|
+
operand.is_a?(Array) && operand.include?(value)
|
|
438
|
+
else
|
|
439
|
+
raise "ERROR: Invalid operator: '#{operator}'"
|
|
440
|
+
end
|
|
441
|
+
rescue ArgumentError, NoMethodError, TypeError
|
|
442
|
+
# Comparing incompatible types, e.g. nil > 1, is simply not a match
|
|
443
|
+
false
|
|
201
444
|
end
|
|
202
445
|
end
|
|
203
446
|
end
|
|
@@ -19,6 +19,7 @@ module OpenC3
|
|
|
19
19
|
response_body = nil
|
|
20
20
|
begin
|
|
21
21
|
endpoint = "/openc3-api/plugins?scope=#{scope}"
|
|
22
|
+
endpoint += "&exclude_default=true" unless default
|
|
22
23
|
uri = URI.parse($api_server.generate_url + endpoint)
|
|
23
24
|
auth = $api_server.generate_auth
|
|
24
25
|
|
|
@@ -30,19 +31,11 @@ module OpenC3
|
|
|
30
31
|
http.request(request) do |response|
|
|
31
32
|
response_body = response.body
|
|
32
33
|
response.value() # Raises an HTTP error if the response is not 2xx (success)
|
|
33
|
-
|
|
34
|
-
if default
|
|
35
|
-
return plugins
|
|
36
|
-
else
|
|
37
|
-
return plugins.select do |plugin|
|
|
38
|
-
!plugin.include?('openc3-cosmos-tool-') and !plugin.include?('openc3-tool-base') and
|
|
39
|
-
!plugin.include?('openc3-cosmos-enterprise-tool-') and !plugin.include?('openc3-enterprise-tool-base')
|
|
40
|
-
end
|
|
41
|
-
end
|
|
34
|
+
return JSON.parse(response.body, allow_nan: true, create_additions: true)
|
|
42
35
|
end
|
|
43
36
|
end
|
|
44
37
|
rescue => e
|
|
45
|
-
raise "
|
|
38
|
+
raise "plugin_list failed due to #{e.formatted}\nURI:#{uri}\nResponse:\n#{response_body}"
|
|
46
39
|
end
|
|
47
40
|
end
|
|
48
41
|
|
|
@@ -65,7 +58,7 @@ module OpenC3
|
|
|
65
58
|
end
|
|
66
59
|
end
|
|
67
60
|
rescue => e
|
|
68
|
-
raise "
|
|
61
|
+
raise "plugin_get failed due to #{e.formatted}\nURI:#{uri}\nResponse:\n#{response_body}"
|
|
69
62
|
end
|
|
70
63
|
end
|
|
71
64
|
|
data/lib/openc3/script/script.rb
CHANGED
|
@@ -313,7 +313,7 @@ module OpenC3
|
|
|
313
313
|
return disconnect if disconnect
|
|
314
314
|
# The only commands allowed through in disconnect mode are read-only
|
|
315
315
|
# Thus we allow the get, list, tlm and limits_enabled and subscribe methods
|
|
316
|
-
if method_name =~ /\w*_get$|^get_\w*|\w*_list$|^list_\w*|^tlm|^limits_enabled
|
|
316
|
+
if method_name =~ /\w*_get$|^get_\w*|\w*_list$|^list_\w*|^tlm|^limits_enabled\??$|^subscribe$/
|
|
317
317
|
return @json_drb.method_missing(method_name, *method_params, **kw_params)
|
|
318
318
|
else
|
|
319
319
|
return nil
|