openc3 7.2.0 → 7.3.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 +108 -29
- data/bin/pipinstall +5 -4
- data/bin/pipuninstall +5 -1
- data/bin/uvinstall +149 -0
- data/data/config/interface_modifiers.yaml +15 -0
- data/data/config/item_modifiers.yaml +2 -1
- data/data/config/microservice.yaml +5 -1
- data/data/config/screen.yaml +2 -1
- data/data/config/target.yaml +38 -0
- data/data/config/tool.yaml +3 -1
- data/data/config/widgets.yaml +43 -3
- data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
- data/data/xtce_schemas/xml.xsd +116 -0
- data/ext/openc3/ext/telemetry/telemetry.c +1 -1
- data/lib/openc3/accessors/binary_accessor.rb +4 -0
- data/lib/openc3/accessors/template_accessor.rb +2 -2
- data/lib/openc3/api/cmd_api.rb +5 -1
- data/lib/openc3/api/interface_api.rb +2 -2
- data/lib/openc3/api/limits_api.rb +89 -3
- data/lib/openc3/api/router_api.rb +3 -3
- data/lib/openc3/api/tlm_api.rb +1 -1
- data/lib/openc3/interfaces/http_client_interface.rb +1 -1
- data/lib/openc3/io/json_drb.rb +24 -8
- data/lib/openc3/io/json_rpc.rb +0 -2
- data/lib/openc3/logs/log_writer.rb +43 -7
- data/lib/openc3/logs/packet_log_writer.rb +1 -2
- data/lib/openc3/microservices/decom_common.rb +15 -4
- data/lib/openc3/microservices/decom_microservice.rb +6 -1
- data/lib/openc3/microservices/interface_microservice.rb +26 -12
- data/lib/openc3/microservices/queue_microservice.rb +4 -2
- data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
- data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
- data/lib/openc3/models/cvt_model.rb +7 -7
- data/lib/openc3/models/db_sharded_model.rb +2 -2
- data/lib/openc3/models/gem_model.rb +1 -1
- data/lib/openc3/models/interface_model.rb +17 -11
- data/lib/openc3/models/interface_status_model.rb +2 -2
- data/lib/openc3/models/metric_model.rb +2 -2
- data/lib/openc3/models/microservice_model.rb +21 -0
- data/lib/openc3/models/microservice_status_model.rb +2 -2
- data/lib/openc3/models/model.rb +2 -6
- data/lib/openc3/models/plugin_model.rb +204 -49
- data/lib/openc3/models/python_package_model.rb +210 -12
- data/lib/openc3/models/reingest_job_model.rb +1 -1
- data/lib/openc3/models/scope_model.rb +34 -30
- data/lib/openc3/models/script_status_model.rb +73 -38
- data/lib/openc3/models/target_model.rb +193 -6
- data/lib/openc3/models/tool_model.rb +33 -20
- data/lib/openc3/operators/microservice_operator.rb +33 -4
- data/lib/openc3/operators/operator.rb +15 -5
- data/lib/openc3/packets/commands.rb +7 -7
- data/lib/openc3/packets/limits.rb +33 -2
- data/lib/openc3/packets/packet.rb +6 -4
- data/lib/openc3/packets/packet_config.rb +5 -1
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
- data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
- data/lib/openc3/packets/structure.rb +16 -12
- data/lib/openc3/packets/telemetry.rb +3 -3
- data/lib/openc3/script/commands.rb +1 -1
- data/lib/openc3/script/extract.rb +20 -25
- data/lib/openc3/script/limits.rb +1 -1
- data/lib/openc3/script/suite_runner.rb +43 -9
- data/lib/openc3/script/web_socket_api.rb +45 -5
- data/lib/openc3/system/target.rb +4 -1
- data/lib/openc3/tools/table_manager/table_config.rb +4 -1
- data/lib/openc3/top_level.rb +1 -1
- data/lib/openc3/topics/command_decom_topic.rb +16 -1
- data/lib/openc3/topics/limits_event_topic.rb +63 -4
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
- data/lib/openc3/utilities/aws_bucket.rb +10 -2
- data/lib/openc3/utilities/cli_generator.rb +5 -1
- data/lib/openc3/utilities/csv.rb +5 -5
- data/lib/openc3/utilities/env_helper.rb +13 -1
- data/lib/openc3/utilities/local_mode.rb +4 -1
- data/lib/openc3/utilities/pypi_url.rb +39 -0
- data/lib/openc3/utilities/reingest_job.rb +1 -1
- data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
- data/lib/openc3/utilities/running_script.rb +160 -100
- data/lib/openc3/utilities/script.rb +60 -13
- data/lib/openc3/utilities/store_autoload.rb +39 -1
- data/lib/openc3/utilities/store_queued.rb +5 -1
- data/lib/openc3/version.rb +5 -5
- data/lib/openc3.rb +6 -1
- data/templates/plugin/plugin.gemspec +1 -1
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -3
- data/templates/widget/package.json +2 -2
- metadata +7 -3
- data/templates/tool_vue/.nycrc +0 -3
- data/templates/widget/.nycrc +0 -3
|
@@ -326,7 +326,7 @@ module OpenC3
|
|
|
326
326
|
# @return [StructureItem] StructureItem or one of its subclasses
|
|
327
327
|
def get_item(name)
|
|
328
328
|
item = @items[name.upcase]
|
|
329
|
-
raise ArgumentError, "Unknown item: #{name}" unless item
|
|
329
|
+
raise ArgumentError, "Unknown item: #{name} (get_item)" unless item
|
|
330
330
|
|
|
331
331
|
return item
|
|
332
332
|
end
|
|
@@ -358,7 +358,7 @@ module OpenC3
|
|
|
358
358
|
# @param name [String] Name of the item to delete in the items Hash
|
|
359
359
|
def delete_item(name)
|
|
360
360
|
item = @items[name.upcase]
|
|
361
|
-
raise ArgumentError, "Unknown item: #{name}" unless item
|
|
361
|
+
raise ArgumentError, "Unknown item: #{name} (delete_item)" unless item
|
|
362
362
|
|
|
363
363
|
# Find the item to delete in the sorted_items array
|
|
364
364
|
item_index = nil
|
|
@@ -604,19 +604,23 @@ module OpenC3
|
|
|
604
604
|
if item.variable_bit_size
|
|
605
605
|
# Bit size is determined by length field
|
|
606
606
|
length_value = self.read(item.variable_bit_size['length_item_name'], :CONVERTED)
|
|
607
|
-
if
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
607
|
+
if length_value
|
|
608
|
+
if (item.data_type == :INT or item.data_type == :UINT) and not item.original_array_size
|
|
609
|
+
case length_value
|
|
610
|
+
when 0
|
|
611
|
+
return 6
|
|
612
|
+
when 1
|
|
613
|
+
return 14
|
|
614
|
+
when 2
|
|
615
|
+
return 30
|
|
616
|
+
else
|
|
617
|
+
return 62
|
|
618
|
+
end
|
|
615
619
|
else
|
|
616
|
-
return
|
|
620
|
+
return (length_value * item.variable_bit_size['length_bits_per_count']) + item.variable_bit_size['length_value_bit_offset']
|
|
617
621
|
end
|
|
618
622
|
else
|
|
619
|
-
|
|
623
|
+
raise "Length value #{item.variable_bit_size['length_item_name']} for item #{item.name} is nil"
|
|
620
624
|
end
|
|
621
625
|
elsif item.original_bit_size <= 0
|
|
622
626
|
# Bit size is full packet length - bits before item + negative bits saved at end
|
|
@@ -59,7 +59,7 @@ module OpenC3
|
|
|
59
59
|
def packets(target_name)
|
|
60
60
|
upcase_target_name = target_name.to_s.upcase
|
|
61
61
|
target_packets = @config.telemetry[upcase_target_name]
|
|
62
|
-
raise "Telemetry target '#{upcase_target_name}' does not exist" unless target_packets
|
|
62
|
+
raise "Telemetry target '#{upcase_target_name}' does not exist (packets lookup)" unless target_packets
|
|
63
63
|
|
|
64
64
|
target_packets
|
|
65
65
|
end
|
|
@@ -177,7 +177,7 @@ module OpenC3
|
|
|
177
177
|
if LATEST_PACKET_NAME.casecmp(packet_name).zero?
|
|
178
178
|
target_upcase = target_name.to_s.upcase
|
|
179
179
|
target_latest_data = @config.latest_data[target_upcase]
|
|
180
|
-
raise "Telemetry
|
|
180
|
+
raise "Telemetry target '#{target_upcase}' does not exist (item_names lookup)" unless target_latest_data
|
|
181
181
|
|
|
182
182
|
item_names = target_latest_data.keys
|
|
183
183
|
else
|
|
@@ -208,7 +208,7 @@ module OpenC3
|
|
|
208
208
|
target_upcase = target_name.to_s.upcase
|
|
209
209
|
item_upcase = item_name.to_s.upcase
|
|
210
210
|
target_latest_data = @config.latest_data[target_upcase]
|
|
211
|
-
raise "Telemetry target '#{target_upcase}' does not exist" unless target_latest_data
|
|
211
|
+
raise "Telemetry target '#{target_upcase}' does not exist (latest_packets lookup)" unless target_latest_data
|
|
212
212
|
|
|
213
213
|
packets = @config.latest_data[target_upcase][item_upcase]
|
|
214
214
|
raise "Telemetry item '#{target_upcase} #{LATEST_PACKET_NAME} #{item_upcase}' does not exist" unless packets
|
|
@@ -106,7 +106,7 @@ module OpenC3
|
|
|
106
106
|
cmd_params.each do |param_name, _param_value|
|
|
107
107
|
param = command['items'].find { |item| item['name'] == param_name }
|
|
108
108
|
unless param
|
|
109
|
-
raise "
|
|
109
|
+
raise "Item '#{target_name} #{cmd_name} #{param_name}' does not exist (command parameter validation)"
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
_log_cmd(command, raw, no_range, no_hazardous)
|
|
@@ -20,7 +20,11 @@ require 'openc3/utilities/store'
|
|
|
20
20
|
|
|
21
21
|
module OpenC3
|
|
22
22
|
module Extract
|
|
23
|
-
|
|
23
|
+
# Tokenizer for command parameters: matches double quoted strings, single quoted strings,
|
|
24
|
+
# bracket delimited arrays (one level of nesting), a bare comma delimiter, or bare words
|
|
25
|
+
# (runs of non-whitespace, non-comma characters). Commas are tokenized separately so
|
|
26
|
+
# whitespace around them is optional.
|
|
27
|
+
SCANNING_REGULAR_EXPRESSION = %r{ (?:"(?:[^\\"]|\\.)*") | (?:'(?:[^\\']|\\.)*') | (?:\[(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*\]) | , | [^\s,]+ }x # "
|
|
24
28
|
|
|
25
29
|
private
|
|
26
30
|
|
|
@@ -88,35 +92,26 @@ module OpenC3
|
|
|
88
92
|
second_half = split_string[1].scan(SCANNING_REGULAR_EXPRESSION)
|
|
89
93
|
keyword = nil
|
|
90
94
|
value = nil
|
|
91
|
-
comma = nil
|
|
92
95
|
second_half.each do |item|
|
|
93
|
-
|
|
96
|
+
if item == ','
|
|
97
|
+
# A comma completes the current keyword / value pair.
|
|
98
|
+
# A comma with nothing pending is a leading or duplicated comma.
|
|
99
|
+
raise "Missing command parameter before comma: #{text}" unless keyword
|
|
100
|
+
raise "Missing value for last command parameter: #{text}" unless value
|
|
101
|
+
add_cmd_parameter(keyword, value, packet, cmd_params)
|
|
102
|
+
keyword = nil
|
|
103
|
+
value = nil
|
|
104
|
+
elsif keyword.nil?
|
|
94
105
|
keyword = item
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
value = item[0..-2]
|
|
100
|
-
comma = true
|
|
101
|
-
else
|
|
102
|
-
value = item
|
|
103
|
-
next
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
unless comma
|
|
107
|
-
raise "Missing comma in command parameters: #{text}" if item != ','
|
|
106
|
+
elsif value.nil?
|
|
107
|
+
value = item
|
|
108
|
+
else
|
|
109
|
+
raise "Missing comma in command parameters: #{text}"
|
|
108
110
|
end
|
|
109
|
-
add_cmd_parameter(keyword, value, packet, cmd_params)
|
|
110
|
-
keyword = nil
|
|
111
|
-
value = nil
|
|
112
|
-
comma = nil
|
|
113
111
|
end
|
|
114
112
|
if keyword
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
else
|
|
118
|
-
raise "Missing value for last command parameter: #{text}"
|
|
119
|
-
end
|
|
113
|
+
raise "Missing value for last command parameter: #{text}" unless value
|
|
114
|
+
add_cmd_parameter(keyword, value, packet, cmd_params)
|
|
120
115
|
end
|
|
121
116
|
end
|
|
122
117
|
|
data/lib/openc3/script/limits.rb
CHANGED
|
@@ -20,7 +20,7 @@ module OpenC3
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
22
|
# Define all the modification methods such that we can disconnect them
|
|
23
|
-
%i(enable_limits disable_limits set_limits enable_limits_group disable_limits_group set_limits_set).each do |method_name|
|
|
23
|
+
%i(enable_limits disable_limits set_limits set_state_color enable_limits_group disable_limits_group set_limits_set delete_limits_set).each do |method_name|
|
|
24
24
|
define_method(method_name) do |*args, **kw_args, &block|
|
|
25
25
|
kw_args[:scope] = $openc3_scope unless kw_args[:scope]
|
|
26
26
|
if $disconnect
|
|
@@ -26,6 +26,11 @@ module OpenC3
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
class SuiteRunner
|
|
29
|
+
# Default suite_runner options shared by all callers (e.g. openc3cli) so
|
|
30
|
+
# the values stay consistent in one place.
|
|
31
|
+
DEFAULT_METHOD = 'start'
|
|
32
|
+
DEFAULT_OPTIONS = ['continueAfterError'].freeze
|
|
33
|
+
|
|
29
34
|
@@suites = []
|
|
30
35
|
@@settings = {}
|
|
31
36
|
@@suite_results = nil
|
|
@@ -42,17 +47,46 @@ module OpenC3
|
|
|
42
47
|
@@suite_results
|
|
43
48
|
end
|
|
44
49
|
|
|
50
|
+
# Validate the suite_runner option combinations that don't require the
|
|
51
|
+
# built suites. Shared by all callers (the running script via execute and
|
|
52
|
+
# openc3cli when constructing options) so the contract lives in one place.
|
|
53
|
+
# Suite/Group existence is checked in execute since it needs @@suites.
|
|
54
|
+
def self.validate_options(group: nil, script: nil)
|
|
55
|
+
raise ArgumentError, "Script #{script} requires a Group" if script and not group
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build the canonical, validated suite_runner hash from raw values,
|
|
59
|
+
# applying defaults. Shared by openc3cli (constructing from CLI flags) and
|
|
60
|
+
# the running script (normalizing a received hash before dispatch) so the
|
|
61
|
+
# hash shape, defaults, and validation live in one place.
|
|
62
|
+
def self.build_options(suite:, group: nil, script: nil, method: nil, options: nil)
|
|
63
|
+
validate_options(group: group, script: script)
|
|
64
|
+
suite_runner = { 'suite' => suite }
|
|
65
|
+
if group
|
|
66
|
+
suite_runner['group'] = group
|
|
67
|
+
suite_runner['script'] = script if script
|
|
68
|
+
end
|
|
69
|
+
# A script always runs via start, matching the GUI
|
|
70
|
+
suite_runner['method'] = script ? DEFAULT_METHOD : (method || DEFAULT_METHOD)
|
|
71
|
+
suite_runner['options'] = options || DEFAULT_OPTIONS.dup
|
|
72
|
+
suite_runner
|
|
73
|
+
end
|
|
74
|
+
|
|
45
75
|
def self.execute(result_string, suite_class, group_class = nil, script = nil)
|
|
46
76
|
@@suite_results = SuiteResults.new
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
# Surface invalid suite / group / option combinations rather than
|
|
78
|
+
# silently running nothing or failing with an opaque nil error. An
|
|
79
|
+
# invalid script method raises later in Group#run_method.
|
|
80
|
+
validate_options(group: group_class, script: script)
|
|
81
|
+
suite = @@suites.find { |s| s.class == suite_class }
|
|
82
|
+
raise ArgumentError, "Suite #{suite_class} not found" unless suite
|
|
83
|
+
if group_class and not suite.scripts.key?(group_class)
|
|
84
|
+
raise ArgumentError, "Group #{group_class} not found in Suite #{suite_class}"
|
|
85
|
+
end
|
|
86
|
+
@@suite_results.start(result_string, suite_class, group_class, script, @@settings)
|
|
87
|
+
loop do
|
|
88
|
+
yield(suite)
|
|
89
|
+
break if not @@settings['Loop'] or (ScriptStatus.instance.fail_count > 0 and @@settings['Break Loop On Error'])
|
|
56
90
|
end
|
|
57
91
|
end
|
|
58
92
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# encoding:
|
|
1
|
+
# encoding: utf-8
|
|
2
2
|
|
|
3
3
|
# Copyright 2026 OpenC3, Inc.
|
|
4
4
|
# All Rights Reserved.
|
|
@@ -22,7 +22,10 @@ module OpenC3
|
|
|
22
22
|
|
|
23
23
|
# Create the WebsocketApi object. If a block is given will automatically connect/disconnect
|
|
24
24
|
def initialize(url:, write_timeout: 10.0, read_timeout: 10.0, connect_timeout: 5.0, authentication: nil, scope: $openc3_scope, &block)
|
|
25
|
-
|
|
25
|
+
# $openc3_scope is only set inside the Script Runner / microservice
|
|
26
|
+
# environment. Fall back to OPENC3_SCOPE (mirrors the Python client) so a
|
|
27
|
+
# bare `ruby script.rb` doesn't send a nil scope that the server rejects.
|
|
28
|
+
@scope = scope || ENV.fetch('OPENC3_SCOPE', 'DEFAULT')
|
|
26
29
|
@authentication = authentication.nil? ? generate_auth() : authentication
|
|
27
30
|
@url = url
|
|
28
31
|
@write_timeout = write_timeout
|
|
@@ -100,6 +103,34 @@ module OpenC3
|
|
|
100
103
|
json_hash['identifier'] = JSON.generate(@identifier, allow_nan: true)
|
|
101
104
|
@stream.write(JSON.generate(json_hash, allow_nan: true))
|
|
102
105
|
@subscribed = true
|
|
106
|
+
wait_for_subscribed()
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Block until the server confirms the subscription. ActionCable / anycable-go
|
|
111
|
+
# process 'subscribe' and 'message' commands as independent RPCs, so an action
|
|
112
|
+
# (add/remove) written immediately after subscribe can reach
|
|
113
|
+
# StreamingChannel#add before the subscription's broadcaster exists, where it
|
|
114
|
+
# is silently dropped (a no-op) and no data ever streams. Waiting for
|
|
115
|
+
# confirm_subscription guarantees the broadcaster is ready before any action
|
|
116
|
+
# is written.
|
|
117
|
+
def wait_for_subscribed
|
|
118
|
+
while true
|
|
119
|
+
message = @stream.read
|
|
120
|
+
raise "WebSocket closed before subscription was confirmed" if message.nil? || message.empty?
|
|
121
|
+
|
|
122
|
+
json_hash = JSON.parse(message, allow_nan: true, create_additions: true)
|
|
123
|
+
case json_hash['type']
|
|
124
|
+
when 'confirm_subscription'
|
|
125
|
+
return
|
|
126
|
+
when 'reject_subscription'
|
|
127
|
+
raise "Subscription Rejected"
|
|
128
|
+
when 'disconnect'
|
|
129
|
+
raise "Unauthorized" if json_hash['reason'] == 'unauthorized'
|
|
130
|
+
else
|
|
131
|
+
# Ignore welcome / ping and keep waiting for confirmation
|
|
132
|
+
next
|
|
133
|
+
end
|
|
103
134
|
end
|
|
104
135
|
end
|
|
105
136
|
|
|
@@ -116,6 +147,13 @@ module OpenC3
|
|
|
116
147
|
|
|
117
148
|
# Send an ActionCable command
|
|
118
149
|
def write_action(data_hash)
|
|
150
|
+
# Subscribe first so the token is present in @identifier before we
|
|
151
|
+
# serialize it below. ActionCable matches a 'message' command to its
|
|
152
|
+
# subscription by the exact identifier string; if subscribe() injected the
|
|
153
|
+
# token only afterward, the message identifier (no token) would not match
|
|
154
|
+
# the subscription identifier (with token) and the server would silently
|
|
155
|
+
# ignore the action.
|
|
156
|
+
subscribe()
|
|
119
157
|
json_hash = {}
|
|
120
158
|
json_hash['command'] = 'message'
|
|
121
159
|
json_hash['identifier'] = JSON.generate(@identifier, allow_nan: true)
|
|
@@ -354,7 +392,8 @@ module OpenC3
|
|
|
354
392
|
# PACKET - Packet name
|
|
355
393
|
# VALUETYPE - RAW, CONVERTED, FORMATTED, or PURE (pure means all types as stored in log)
|
|
356
394
|
#
|
|
357
|
-
def add(items: nil, packets: nil, start_time: nil, end_time: nil, scope:
|
|
395
|
+
def add(items: nil, packets: nil, start_time: nil, end_time: nil, scope: nil)
|
|
396
|
+
scope ||= @scope
|
|
358
397
|
data_hash = {}
|
|
359
398
|
data_hash['action'] = 'add'
|
|
360
399
|
if start_time
|
|
@@ -395,7 +434,8 @@ module OpenC3
|
|
|
395
434
|
# PACKET - Packet name
|
|
396
435
|
# VALUETYPE - RAW, CONVERTED, FORMATTED, or PURE (pure means all types as stored in log)
|
|
397
436
|
#
|
|
398
|
-
def remove(items: nil, packets: nil, scope:
|
|
437
|
+
def remove(items: nil, packets: nil, scope: nil)
|
|
438
|
+
scope ||= @scope
|
|
399
439
|
data_hash = {}
|
|
400
440
|
data_hash['action'] = 'remove'
|
|
401
441
|
data_hash['items'] = items if items
|
|
@@ -407,7 +447,7 @@ module OpenC3
|
|
|
407
447
|
|
|
408
448
|
# Convenience method to read all data until end marker is received.
|
|
409
449
|
# Warning: DATA IS STORED IN RAM. Do not use this with large queries
|
|
410
|
-
def self.read_all(items: nil, packets: nil, start_time: nil, end_time:, scope:
|
|
450
|
+
def self.read_all(items: nil, packets: nil, start_time: nil, end_time:, scope: nil, timeout: nil)
|
|
411
451
|
read_all_start_time = Time.now
|
|
412
452
|
data = []
|
|
413
453
|
self.new do |api|
|
data/lib/openc3/system/target.rb
CHANGED
|
@@ -102,7 +102,10 @@ module OpenC3
|
|
|
102
102
|
def process_file(filename)
|
|
103
103
|
Logger.instance.info "Processing target definition in file '#{filename}'"
|
|
104
104
|
parser = ConfigParser.new("https://docs.openc3.com/docs/configuration/target")
|
|
105
|
-
|
|
105
|
+
# run_erb is false: ERB is only rendered once, at plugin install time
|
|
106
|
+
# (TargetModel#deploy). At runtime target files are read already-rendered
|
|
107
|
+
# from the bucket, so they are treated as data, not re-executed as code.
|
|
108
|
+
parser.parse_file(filename, false, true, false) do |keyword, parameters|
|
|
106
109
|
case keyword
|
|
107
110
|
when 'LANGUAGE'
|
|
108
111
|
usage = "#{keyword} <ruby | python>"
|
|
@@ -85,7 +85,10 @@ module OpenC3
|
|
|
85
85
|
ConfigParser.new(
|
|
86
86
|
'https://openc3.com/docs/tools/#table-manager-configuration-openc3--39',
|
|
87
87
|
)
|
|
88
|
-
|
|
88
|
+
# run_erb is false: ERB is only rendered once, at plugin install time
|
|
89
|
+
# (TargetModel#deploy). Table definitions are read already-rendered from the
|
|
90
|
+
# bucket here, so they are treated as data, not re-executed as code.
|
|
91
|
+
parser.parse_file(filename, false, true, false) do |keyword, params|
|
|
89
92
|
if @building_generic_conversion
|
|
90
93
|
case keyword
|
|
91
94
|
# Complete a generic conversion
|
data/lib/openc3/top_level.rb
CHANGED
|
@@ -108,7 +108,7 @@ module OpenC3
|
|
|
108
108
|
# This method is taken directly from the Rails source:
|
|
109
109
|
# https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Filename.html#method-i-sanitized
|
|
110
110
|
# NOTE: I removed the '/' character because we have to allow this in order to traverse the path
|
|
111
|
-
sanitized = path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "
|
|
111
|
+
sanitized = path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\u{FFFD}").strip.tr("\u{202E}%$|:;\t\r\n\\", "-").gsub('..', '-')
|
|
112
112
|
if sanitized != path
|
|
113
113
|
raise StorageError, "Invalid path: #{path}"
|
|
114
114
|
end
|
|
@@ -30,13 +30,28 @@ module OpenC3
|
|
|
30
30
|
received_count: packet.received_count }
|
|
31
31
|
# Read all RAW values at once - optimized by accessor
|
|
32
32
|
json_hash = packet.read_items(packet.sorted_items)
|
|
33
|
+
# Items with a write conversion have already been transformed by the time the
|
|
34
|
+
# value lands in the buffer, so reading the buffer back does not recover what
|
|
35
|
+
# the user actually commanded. Use the given values (as passed to build_cmd)
|
|
36
|
+
# for those items. State items are the exception: the user can give either the
|
|
37
|
+
# state name or the state value, so they always read from the buffer to log the
|
|
38
|
+
# normalized state name. Raw commands bypass the write conversions entirely so
|
|
39
|
+
# their given values are raw and must not be logged as converted.
|
|
40
|
+
given_values = packet.raw ? nil : packet.given_values
|
|
41
|
+
given_values = given_values.transform_keys { |key| key.to_s.upcase } if given_values
|
|
33
42
|
# Read additional value types using given_raw to avoid re-reading from buffer
|
|
34
43
|
packet.sorted_items.each do |item|
|
|
35
44
|
if item.hidden
|
|
36
45
|
json_hash.delete(item.name)
|
|
37
46
|
else
|
|
38
47
|
given_raw = json_hash[item.name]
|
|
39
|
-
|
|
48
|
+
if item.write_conversion or item.states
|
|
49
|
+
if item.write_conversion and !item.states and given_values and given_values.key?(item.name)
|
|
50
|
+
json_hash[item.name + "__C"] = given_values[item.name]
|
|
51
|
+
else
|
|
52
|
+
json_hash[item.name + "__C"] = packet.read_item(item, :CONVERTED, packet.buffer, given_raw)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
40
55
|
json_hash[item.name + "__F"] = packet.read_item(item, :FORMATTED, packet.buffer, given_raw) if item.format_string
|
|
41
56
|
end
|
|
42
57
|
end
|
|
@@ -39,9 +39,15 @@ module OpenC3
|
|
|
39
39
|
case event[:type]
|
|
40
40
|
when :LIMITS_CHANGE
|
|
41
41
|
# The current_limits hash keeps only the current limits state of items
|
|
42
|
-
# It is used by the API to determine the overall limits state
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
# It is used by the API to determine the overall limits state.
|
|
43
|
+
# When the event originates from a stored packet in a non-PROCESS mode
|
|
44
|
+
# (LOG or DISABLE), skip updating current_limits so that historical
|
|
45
|
+
# data does not affect the real-time overall limits state or the
|
|
46
|
+
# out_of_limits API response.
|
|
47
|
+
unless event[:suppress_stored]
|
|
48
|
+
field = "#{event[:target_name]}__#{event[:packet_name]}__#{event[:item_name]}"
|
|
49
|
+
Store.hset("#{scope}__current_limits", field, event[:new_limits_state])
|
|
50
|
+
end
|
|
45
51
|
|
|
46
52
|
when :LIMITS_SETTINGS
|
|
47
53
|
# Limits updated in limits_api.rb to avoid circular reference to TargetModel
|
|
@@ -79,9 +85,22 @@ module OpenC3
|
|
|
79
85
|
limits_settings['enabled'] = event[:enabled]
|
|
80
86
|
Store.hset("#{scope}__current_limits_settings", field, JSON.generate(limits_settings, allow_nan: true))
|
|
81
87
|
|
|
88
|
+
when :LIMITS_STATE_COLOR
|
|
89
|
+
# Persist the state color so it survives a decom microservice restart (applied by sync_system)
|
|
90
|
+
field = "#{event[:target_name]}__#{event[:packet_name]}__#{event[:item_name]}"
|
|
91
|
+
limits_settings = Store.hget("#{scope}__current_limits_settings", field)
|
|
92
|
+
if limits_settings
|
|
93
|
+
limits_settings = JSON.parse(limits_settings, allow_nan: true, create_additions: true)
|
|
94
|
+
else
|
|
95
|
+
limits_settings = {}
|
|
96
|
+
end
|
|
97
|
+
limits_settings['state_colors'] ||= {}
|
|
98
|
+
limits_settings['state_colors'][event[:state_name].to_s] = event[:color].to_s
|
|
99
|
+
Store.hset("#{scope}__current_limits_settings", field, JSON.generate(limits_settings, allow_nan: true))
|
|
100
|
+
|
|
82
101
|
when :LIMITS_SET
|
|
83
102
|
sets = sets(scope: scope)
|
|
84
|
-
raise "
|
|
103
|
+
raise "Limits set '#{event[:set]}' does not exist" unless sets.key?(event[:set])
|
|
85
104
|
|
|
86
105
|
# Set all existing sets to "false"
|
|
87
106
|
sets = sets.transform_values! { |_key, _value| "false" }
|
|
@@ -173,6 +192,28 @@ module OpenC3
|
|
|
173
192
|
end
|
|
174
193
|
end
|
|
175
194
|
|
|
195
|
+
# Removes a limits set from the limits_sets hash and from the
|
|
196
|
+
# current_limits_settings of every item. Note that the set is not removed
|
|
197
|
+
# from the TargetModel packet definitions; that is cleaned up on the next
|
|
198
|
+
# plugin install. Running microservices will continue to hold the set in
|
|
199
|
+
# memory until they restart and resync from current_limits_settings.
|
|
200
|
+
def self.delete_set(set_name, scope:)
|
|
201
|
+
set_name = set_name.to_s
|
|
202
|
+
limits_settings = Store.hgetall("#{scope}__current_limits_settings")
|
|
203
|
+
# Collect all changed items and write them back in a single hmset to
|
|
204
|
+
# avoid a Redis round trip per item (this hash can be large)
|
|
205
|
+
updates = {}
|
|
206
|
+
limits_settings.each do |item, settings|
|
|
207
|
+
settings = JSON.parse(settings, allow_nan: true, create_additions: true)
|
|
208
|
+
if settings.key?(set_name)
|
|
209
|
+
settings.delete(set_name)
|
|
210
|
+
updates[item] = JSON.generate(settings, allow_nan: true)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
Store.hmset("#{scope}__current_limits_settings", *updates.flatten) unless updates.empty?
|
|
214
|
+
Store.hdel("#{scope}__limits_sets", set_name)
|
|
215
|
+
end
|
|
216
|
+
|
|
176
217
|
# Update the local System based on overall state
|
|
177
218
|
def self.sync_system(scope:)
|
|
178
219
|
all_limits_settings = Store.hgetall("#{scope}__current_limits_settings")
|
|
@@ -188,6 +229,12 @@ module OpenC3
|
|
|
188
229
|
persistence = limits_settings['persistence_setting']
|
|
189
230
|
limits_settings.each do |limits_set, settings|
|
|
190
231
|
next unless Hash === settings
|
|
232
|
+
if limits_set == 'state_colors'
|
|
233
|
+
settings.each do |state_name, color|
|
|
234
|
+
System.limits.set_state_color(target_name, packet_name, item_name, state_name, color)
|
|
235
|
+
end
|
|
236
|
+
next
|
|
237
|
+
end
|
|
191
238
|
System.limits.set(target_name, packet_name, item_name, settings['red_low'], settings['yellow_low'], settings['yellow_high'], settings['red_high'], settings['green_low'], settings['green_high'], limits_set.to_s.intern, persistence, enabled)
|
|
192
239
|
end
|
|
193
240
|
if not enabled.nil?
|
|
@@ -242,6 +289,18 @@ module OpenC3
|
|
|
242
289
|
end
|
|
243
290
|
end
|
|
244
291
|
|
|
292
|
+
when 'LIMITS_STATE_COLOR'
|
|
293
|
+
target_name = event['target_name']
|
|
294
|
+
packet_name = event['packet_name']
|
|
295
|
+
item_name = event['item_name']
|
|
296
|
+
target = telemetry[target_name]
|
|
297
|
+
if target
|
|
298
|
+
packet = target[packet_name]
|
|
299
|
+
if packet
|
|
300
|
+
System.limits.set_state_color(target_name, packet_name, item_name, event['state_name'], event['color'])
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
245
304
|
when 'LIMITS_SET'
|
|
246
305
|
System.limits_set = event['set']
|
|
247
306
|
end
|
|
@@ -20,7 +20,7 @@ require 'openc3/utilities/open_telemetry'
|
|
|
20
20
|
|
|
21
21
|
module OpenC3
|
|
22
22
|
class TelemetryDecomTopic < Topic
|
|
23
|
-
def self.write_packet(packet, id: nil, scope:)
|
|
23
|
+
def self.write_packet(packet, id: nil, include_limits_states: true, scope:)
|
|
24
24
|
OpenC3.in_span("write_packet") do
|
|
25
25
|
# Need to build a JSON hash of the decommutated data
|
|
26
26
|
# Support "downward typing"
|
|
@@ -29,7 +29,7 @@ module OpenC3
|
|
|
29
29
|
# If nothing - item does not exist - nil
|
|
30
30
|
# __ as separators ITEM1, ITEM1__C, ITEM1__F
|
|
31
31
|
|
|
32
|
-
json_hash = CvtModel.build_json_from_packet(packet)
|
|
32
|
+
json_hash = CvtModel.build_json_from_packet(packet, include_limits_states: include_limits_states)
|
|
33
33
|
# Convert to JSON-safe types once and reuse for both topic write and CVT set
|
|
34
34
|
json_safe_hash = json_hash.as_json
|
|
35
35
|
json_data = JSON.generate(json_safe_hash, allow_nan: true)
|
|
@@ -213,6 +213,14 @@ module OpenC3
|
|
|
213
213
|
# If the key is not found return nil
|
|
214
214
|
rescue Aws::S3::Errors::NoSuchKey
|
|
215
215
|
nil
|
|
216
|
+
# AWS S3 returns 403 AccessDenied rather than 404 NoSuchKey when the caller
|
|
217
|
+
# does not have s3:ListBucket on the bucket. Callers which probe for an
|
|
218
|
+
# optional key (i.e. TargetFile.body checking targets_modified first) would
|
|
219
|
+
# otherwise raise instead of falling through, so treat it as not found.
|
|
220
|
+
# Warn since a genuine permission problem looks identical from here.
|
|
221
|
+
rescue Aws::S3::Errors::AccessDenied
|
|
222
|
+
Logger.warn("Access denied reading #{bucket}/#{key}. Treating as not found. Verify bucket permissions if this key should exist.")
|
|
223
|
+
nil
|
|
216
224
|
end
|
|
217
225
|
|
|
218
226
|
def list_objects(bucket:, prefix: nil, max_request: 1000, max_total: 100_000)
|
|
@@ -233,7 +241,7 @@ module OpenC3
|
|
|
233
241
|
# Array of objects with key and size methods
|
|
234
242
|
result
|
|
235
243
|
rescue Aws::S3::Errors::NoSuchBucket
|
|
236
|
-
raise NotFound, "Bucket '#{bucket}' does not exist
|
|
244
|
+
raise NotFound, "Bucket '#{bucket}' does not exist (list_objects)"
|
|
237
245
|
end
|
|
238
246
|
|
|
239
247
|
# Lists the files under a specified path
|
|
@@ -283,7 +291,7 @@ module OpenC3
|
|
|
283
291
|
end
|
|
284
292
|
result
|
|
285
293
|
rescue Aws::S3::Errors::NoSuchBucket
|
|
286
|
-
raise NotFound, "Bucket '#{bucket}' does not exist
|
|
294
|
+
raise NotFound, "Bucket '#{bucket}' does not exist (list_files)"
|
|
287
295
|
end
|
|
288
296
|
|
|
289
297
|
# get metadata for a specific object
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# encoding:
|
|
1
|
+
# encoding: utf-8
|
|
2
2
|
|
|
3
3
|
# Copyright 2026 OpenC3, Inc.
|
|
4
4
|
# All Rights Reserved.
|
|
@@ -478,6 +478,10 @@ RUBY
|
|
|
478
478
|
VARIABLE #{target_name.downcase}_target_name #{target_name}
|
|
479
479
|
|
|
480
480
|
TARGET #{target_name} <%= #{target_name.downcase}_target_name %>
|
|
481
|
+
# Decom tlm and cmd data in the time-series database is kept forever by default.
|
|
482
|
+
# Uncomment to enforce a 30 day retention time. See https://docs.openc3.com/docs/configuration/plugins
|
|
483
|
+
# CMD_DECOM_RETAIN_TIME 30d
|
|
484
|
+
# TLM_DECOM_RETAIN_TIME 30d
|
|
481
485
|
#{interface_line}
|
|
482
486
|
MAP_TARGET <%= #{target_name.downcase}_target_name %>
|
|
483
487
|
DOC
|
data/lib/openc3/utilities/csv.rb
CHANGED
|
@@ -59,7 +59,7 @@ module OpenC3
|
|
|
59
59
|
# @param index [Integer] Which value to return
|
|
60
60
|
# @return [Boolean] Single value converted to a boolean (true or false)
|
|
61
61
|
def bool(item, index = 0)
|
|
62
|
-
raise "#{item} not found" unless keys.include?(item)
|
|
62
|
+
raise "CSV key '#{item}' not found for bool conversion" unless keys.include?(item)
|
|
63
63
|
|
|
64
64
|
if Range === index
|
|
65
65
|
@hash[item][index].map do |x|
|
|
@@ -91,7 +91,7 @@ module OpenC3
|
|
|
91
91
|
# @param index [Integer] Which value to return
|
|
92
92
|
# @return [Integer] Single value converted to an integer
|
|
93
93
|
def int(item, index = 0)
|
|
94
|
-
raise "#{item} not found" unless keys.include?(item)
|
|
94
|
+
raise "CSV key '#{item}' not found for int conversion" unless keys.include?(item)
|
|
95
95
|
|
|
96
96
|
if Range === index
|
|
97
97
|
@hash[item][index].map { |x| x.to_i }
|
|
@@ -107,7 +107,7 @@ module OpenC3
|
|
|
107
107
|
# @param index [Integer] Which value to return
|
|
108
108
|
# @return [Float] Single value converted to a float
|
|
109
109
|
def float(item, index = 0)
|
|
110
|
-
raise "#{item} not found" unless keys.include?(item)
|
|
110
|
+
raise "CSV key '#{item}' not found for float conversion" unless keys.include?(item)
|
|
111
111
|
|
|
112
112
|
if Range === index
|
|
113
113
|
@hash[item][index].map { |x| x.to_f }
|
|
@@ -122,7 +122,7 @@ module OpenC3
|
|
|
122
122
|
# @param index [Integer] Which value to return
|
|
123
123
|
# @return [String] Single value converted to a string
|
|
124
124
|
def string(item, index = 0)
|
|
125
|
-
raise "#{item} not found" unless keys.include?(item)
|
|
125
|
+
raise "CSV key '#{item}' not found for string lookup" unless keys.include?(item)
|
|
126
126
|
|
|
127
127
|
if Range === index
|
|
128
128
|
@hash[item][index].map { |x| x.to_s }
|
|
@@ -138,7 +138,7 @@ module OpenC3
|
|
|
138
138
|
# @param index [Integer] Which value to return
|
|
139
139
|
# @return [Symbol] Single value converted to a symbol
|
|
140
140
|
def symbol(item, index = 0)
|
|
141
|
-
raise "#{item} not found" unless keys.include?(item)
|
|
141
|
+
raise "CSV key '#{item}' not found for symbol lookup" unless keys.include?(item)
|
|
142
142
|
|
|
143
143
|
if Range === index
|
|
144
144
|
@hash[item][index].map { |x| x.intern }
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# encoding: ascii-8bit
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 OpenC3, Inc.
|
|
4
|
+
# All Rights Reserved.
|
|
5
|
+
#
|
|
6
|
+
# This program is distributed in the hope that it will be useful,
|
|
7
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
8
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
9
|
+
# See LICENSE.md for more details.
|
|
10
|
+
#
|
|
11
|
+
# This file may also be used under the terms of a commercial license
|
|
12
|
+
# if purchased from OpenC3, Inc.
|
|
13
|
+
|
|
2
14
|
class EnvHelper
|
|
3
15
|
def self.enabled?(key)
|
|
4
16
|
['true', '1', 'yes', 'on'].include?(ENV[key].to_s.downcase)
|
|
@@ -485,7 +485,10 @@ module OpenC3
|
|
|
485
485
|
config_path = "#{OPENC3_LOCAL_MODE_PATH}/#{scope}/settings/#{name}.json"
|
|
486
486
|
return unless File.expand_path(config_path).start_with?(OPENC3_LOCAL_MODE_PATH)
|
|
487
487
|
FileUtils.mkdir_p(File.dirname(config_path))
|
|
488
|
-
# Anything can be stored as a setting
|
|
488
|
+
# Anything can be stored as a setting. Strings are written directly since
|
|
489
|
+
# they're already the serialized form. Anything else is written as JSON so
|
|
490
|
+
# sync_settings can read it back rather than a Ruby inspect string.
|
|
491
|
+
data = JSON.generate(data.as_json(:allow_nan => true)) unless String === data
|
|
489
492
|
File.write(config_path, data)
|
|
490
493
|
end
|
|
491
494
|
|