openc3 7.1.1 → 7.2.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/openc3cli +11 -23
- data/data/config/command_modifiers.yaml +2 -2
- data/data/config/interface_modifiers.yaml +15 -0
- data/data/config/item_modifiers.yaml +10 -3
- data/data/config/target.yaml +17 -0
- data/data/config/widgets.yaml +28 -1
- data/lib/openc3/accessors/binary_accessor.rb +4 -0
- data/lib/openc3/api/cmd_api.rb +5 -1
- data/lib/openc3/api/limits_api.rb +72 -0
- data/lib/openc3/api/tlm_api.rb +6 -0
- 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 +25 -11
- data/lib/openc3/microservices/microservice.rb +20 -5
- data/lib/openc3/microservices/queue_microservice.rb +4 -2
- data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
- data/lib/openc3/models/cvt_model.rb +2 -2
- data/lib/openc3/models/db_sharded_model.rb +2 -2
- data/lib/openc3/models/interface_model.rb +16 -10
- data/lib/openc3/models/interface_status_model.rb +2 -2
- data/lib/openc3/models/metric_model.rb +2 -2
- 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 +15 -14
- data/lib/openc3/models/python_package_model.rb +3 -1
- data/lib/openc3/models/reingest_job_model.rb +1 -1
- data/lib/openc3/models/script_status_model.rb +73 -38
- data/lib/openc3/models/target_model.rb +13 -0
- data/lib/openc3/operators/operator.rb +49 -14
- data/lib/openc3/packets/commands.rb +3 -3
- data/lib/openc3/packets/limits.rb +23 -0
- data/lib/openc3/packets/packet.rb +5 -3
- data/lib/openc3/packets/packet_config.rb +22 -5
- data/lib/openc3/packets/structure.rb +14 -10
- data/lib/openc3/script/limits.rb +1 -1
- data/lib/openc3/script/suite.rb +1 -1
- data/lib/openc3/script/suite_runner.rb +43 -9
- data/lib/openc3/script/web_socket_api.rb +6 -2
- 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/limits_event_topic.rb +62 -3
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
- data/lib/openc3/utilities/cli_generator.rb +432 -404
- data/lib/openc3/utilities/env_helper.rb +13 -1
- data/lib/openc3/utilities/pypi_url.rb +39 -0
- data/lib/openc3/utilities/questdb_client.rb +51 -4
- data/lib/openc3/utilities/reingest_job.rb +1 -1
- data/lib/openc3/utilities/running_script.rb +58 -15
- data/lib/openc3/utilities/script.rb +14 -3
- data/lib/openc3/utilities/simulated_target.rb +4 -2
- 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/command_validator/command_validator.py +8 -10
- data/templates/command_validator/command_validator.rb +6 -9
- data/templates/plugin/LICENSE.md +16 -4
- 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 +3 -1
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
|
data/lib/openc3/script/suite.rb
CHANGED
|
@@ -287,7 +287,7 @@ module OpenC3
|
|
|
287
287
|
# Find all the script methods
|
|
288
288
|
methods = []
|
|
289
289
|
self.instance_methods.each do |method_name|
|
|
290
|
-
if /^
|
|
290
|
+
if /^test_|^script_|^op_/.match?(method_name.to_s)
|
|
291
291
|
methods << method_name.to_s
|
|
292
292
|
end
|
|
293
293
|
end
|
|
@@ -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.
|
|
@@ -91,6 +91,10 @@ module OpenC3
|
|
|
91
91
|
# Will subscribe to the channel based on @identifier
|
|
92
92
|
def subscribe
|
|
93
93
|
unless @subscribed
|
|
94
|
+
# Token is part of the identifier so it surfaces as params[:token] in
|
|
95
|
+
# ApplicationCable::Channel#authenticate_subscription! — ActionCable
|
|
96
|
+
# ignores `data` on `subscribe` commands.
|
|
97
|
+
@identifier['token'] = @authentication.token(include_bearer: false)
|
|
94
98
|
json_hash = {}
|
|
95
99
|
json_hash['command'] = 'subscribe'
|
|
96
100
|
json_hash['identifier'] = JSON.generate(@identifier, allow_nan: true)
|
|
@@ -128,7 +132,7 @@ module OpenC3
|
|
|
128
132
|
# Connect to the websocket with authorization in query params
|
|
129
133
|
def connect
|
|
130
134
|
disconnect()
|
|
131
|
-
final_url = @url + "?scope=#{@scope}
|
|
135
|
+
final_url = @url + "?scope=#{@scope}"
|
|
132
136
|
@stream = WebSocketClientStream.new(final_url, @write_timeout, @read_timeout, @connect_timeout)
|
|
133
137
|
@stream.headers = {
|
|
134
138
|
'Sec-WebSocket-Protocol' => 'actioncable-v1-json, actioncable-unsupported',
|
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
|
|
@@ -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,6 +85,19 @@ 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
103
|
raise "Set '#{event[:set]}' does not exist!" unless sets.key?(event[:set])
|
|
@@ -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)
|