openc3 7.0.1 → 7.1.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 +47 -3
- data/data/config/item_modifiers.yaml +1 -1
- data/data/config/microservice.yaml +12 -1
- data/data/config/parameter_modifiers.yaml +49 -7
- data/data/config/target.yaml +11 -0
- data/data/config/target_config.yaml +6 -2
- data/lib/openc3/api/cmd_api.rb +2 -1
- data/lib/openc3/api/metrics_api.rb +11 -1
- data/lib/openc3/api/tlm_api.rb +21 -6
- data/lib/openc3/core_ext/faraday.rb +1 -1
- data/lib/openc3/io/json_api.rb +1 -1
- data/lib/openc3/logs/log_writer.rb +3 -1
- data/lib/openc3/microservices/decom_common.rb +128 -0
- data/lib/openc3/microservices/decom_microservice.rb +26 -95
- data/lib/openc3/microservices/interface_decom_common.rb +6 -2
- data/lib/openc3/microservices/interface_microservice.rb +10 -8
- data/lib/openc3/microservices/log_microservice.rb +1 -1
- data/lib/openc3/microservices/microservice.rb +3 -2
- data/lib/openc3/microservices/queue_microservice.rb +1 -1
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
- data/lib/openc3/microservices/text_log_microservice.rb +1 -2
- data/lib/openc3/models/cvt_model.rb +24 -13
- data/lib/openc3/models/db_sharded_model.rb +110 -0
- data/lib/openc3/models/interface_model.rb +9 -0
- data/lib/openc3/models/interface_status_model.rb +33 -3
- data/lib/openc3/models/metric_model.rb +96 -37
- data/lib/openc3/models/microservice_model.rb +7 -0
- data/lib/openc3/models/microservice_status_model.rb +30 -3
- data/lib/openc3/models/reingest_job_model.rb +153 -0
- data/lib/openc3/models/scope_model.rb +3 -2
- data/lib/openc3/models/script_status_model.rb +4 -20
- data/lib/openc3/models/target_model.rb +113 -100
- data/lib/openc3/packets/packet_config.rb +4 -1
- data/lib/openc3/script/script.rb +2 -2
- data/lib/openc3/script/script_runner.rb +4 -4
- data/lib/openc3/script/telemetry.rb +3 -3
- data/lib/openc3/script/web_socket_api.rb +29 -22
- data/lib/openc3/system/system.rb +20 -3
- data/lib/openc3/topics/command_decom_topic.rb +4 -2
- data/lib/openc3/topics/command_topic.rb +8 -5
- data/lib/openc3/topics/decom_interface_topic.rb +15 -10
- data/lib/openc3/topics/interface_topic.rb +71 -29
- data/lib/openc3/topics/limits_event_topic.rb +62 -41
- data/lib/openc3/topics/router_topic.rb +61 -21
- data/lib/openc3/topics/system_events_topic.rb +18 -1
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -1
- data/lib/openc3/topics/telemetry_topic.rb +4 -2
- data/lib/openc3/topics/topic.rb +77 -5
- data/lib/openc3/utilities/aws_bucket.rb +2 -0
- data/lib/openc3/utilities/cli_generator.rb +3 -2
- data/lib/openc3/utilities/metric.rb +15 -1
- data/lib/openc3/utilities/questdb_client.rb +173 -37
- data/lib/openc3/utilities/reingest_job.rb +377 -0
- data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
- data/lib/openc3/utilities/store_autoload.rb +78 -52
- data/lib/openc3/utilities/store_queued.rb +20 -12
- data/lib/openc3/version.rb +6 -6
- data/templates/plugin/plugin.gemspec +13 -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/tool_vue/src/router.js +2 -2
- data/templates/widget/package.json +2 -2
- metadata +7 -3
data/lib/openc3/system/system.rb
CHANGED
|
@@ -77,7 +77,8 @@ module OpenC3
|
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
# target_version can also be the actual hash used in the target_archives folder
|
|
81
|
+
def self.setup_targets(target_names, base_dir, target_version: 'current', scope:)
|
|
81
82
|
# Nothing to do if there are no targets
|
|
82
83
|
return if target_names.nil? or target_names.length == 0
|
|
83
84
|
if @@instance.nil?
|
|
@@ -85,10 +86,15 @@ module OpenC3
|
|
|
85
86
|
FileUtils.mkdir_p(targets_path)
|
|
86
87
|
bucket = Bucket.getClient()
|
|
87
88
|
target_names.each do |target_name|
|
|
89
|
+
# Remove any prior extraction so re-running setup_targets (e.g. after
|
|
90
|
+
# reset_instance! during reingest) starts from a clean slate. Without
|
|
91
|
+
# this, Zip::File#extract fails on files left behind by a previous run.
|
|
92
|
+
FileUtils.rm_rf("#{targets_path}/#{target_name}")
|
|
93
|
+
|
|
88
94
|
# Retrieve bucket/targets/target_name/<TARGET>_current.zip
|
|
89
|
-
zip_path = "#{targets_path}/#{target_name}
|
|
95
|
+
zip_path = "#{targets_path}/#{target_name}_#{target_version}.zip"
|
|
90
96
|
FileUtils.mkdir_p(File.dirname(zip_path))
|
|
91
|
-
bucket_key = "#{scope}/target_archives/#{target_name}/#{target_name}
|
|
97
|
+
bucket_key = "#{scope}/target_archives/#{target_name}/#{target_name}_#{target_version}.zip"
|
|
92
98
|
Logger.info("Retrieving #{bucket_key} from targets bucket")
|
|
93
99
|
bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: bucket_key, path: zip_path)
|
|
94
100
|
Zip::File.open(zip_path) do |zip_file|
|
|
@@ -116,6 +122,17 @@ module OpenC3
|
|
|
116
122
|
end
|
|
117
123
|
end
|
|
118
124
|
|
|
125
|
+
# Clears the System singleton so the next call to setup_targets or
|
|
126
|
+
# instance rebuilds it. Intended for admin flows (e.g. reingest) that
|
|
127
|
+
# need to load a specific target_version distinct from whatever is
|
|
128
|
+
# currently loaded. Callers must hold an external lock if they need to
|
|
129
|
+
# protect other threads from observing a nil @@instance briefly.
|
|
130
|
+
def self.reset_instance!
|
|
131
|
+
@@instance_mutex.synchronize do
|
|
132
|
+
@@instance = nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
119
136
|
# Get the singleton instance of System
|
|
120
137
|
#
|
|
121
138
|
# @param target_names [Array of target_names]
|
|
@@ -42,11 +42,13 @@ module OpenC3
|
|
|
42
42
|
end
|
|
43
43
|
msg_hash['json_data'] = JSON.generate(json_hash.as_json, allow_nan: true)
|
|
44
44
|
msg_hash['extra'] = JSON.generate(packet.extra.as_json, allow_nan: true) if packet.extra
|
|
45
|
-
|
|
45
|
+
db_shard = Store.db_shard_for_target(packet.target_name, scope: scope)
|
|
46
|
+
EphemeralStoreQueued.instance(db_shard: db_shard).write_topic(topic, msg_hash)
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def self.get_cmd_item(target_name, packet_name, param_name, type: :FORMATTED, scope: $openc3_scope)
|
|
49
|
-
|
|
50
|
+
db_shard = Store.db_shard_for_target(target_name, scope: scope)
|
|
51
|
+
msg_id, msg_hash = Topic.get_newest_message("#{scope}__DECOMCMD__{#{target_name}}__#{packet_name}", db_shard: db_shard)
|
|
50
52
|
if msg_id
|
|
51
53
|
if param_name == 'RECEIVED_COUNT'
|
|
52
54
|
msg_hash['received_count'].to_i
|
|
@@ -33,7 +33,8 @@ module OpenC3
|
|
|
33
33
|
received_count: packet.received_count,
|
|
34
34
|
stored: packet.stored.to_s,
|
|
35
35
|
buffer: packet.buffer(false) }
|
|
36
|
-
|
|
36
|
+
db_shard = Store.db_shard_for_target(packet.target_name, scope: scope)
|
|
37
|
+
EphemeralStoreQueued.instance(db_shard: db_shard).write_topic(topic, msg_hash)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# @param command [Hash] Command hash structure read to be written to a topic
|
|
@@ -45,20 +46,22 @@ module OpenC3
|
|
|
45
46
|
command['cmd_params'] = JSON.generate(command['cmd_params'].as_json, allow_nan: true)
|
|
46
47
|
OpenC3.inject_context(command)
|
|
47
48
|
|
|
49
|
+
db_shard = Store.db_shard_for_target(command['target_name'], scope: scope)
|
|
50
|
+
|
|
48
51
|
# Fire-and-forget mode: skip ACK waiting when timeout <= 0
|
|
49
52
|
if timeout <= 0
|
|
50
|
-
Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100)
|
|
53
|
+
Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100, db_shard: db_shard)
|
|
51
54
|
command["cmd_params"] = cmd_params # Restore the original cmd_params Hash
|
|
52
55
|
return command
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
ack_topic = "{#{scope}__ACKCMD}TARGET__#{command['target_name']}"
|
|
56
|
-
Topic.update_topic_offsets([ack_topic])
|
|
57
|
-
cmd_id = Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100)
|
|
59
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
60
|
+
cmd_id = Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100, db_shard: db_shard)
|
|
58
61
|
command["cmd_params"] = cmd_params # Restore the original cmd_params Hash
|
|
59
62
|
time = Time.now
|
|
60
63
|
while (Time.now - time) < timeout
|
|
61
|
-
Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
|
|
64
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
62
65
|
if msg_hash["id"] == cmd_id
|
|
63
66
|
if msg_hash["result"] == "SUCCESS"
|
|
64
67
|
return command
|
|
@@ -26,13 +26,14 @@ module OpenC3
|
|
|
26
26
|
# DecomMicroservice is listening to the DECOMINTERFACE topic and is responsible
|
|
27
27
|
# for actually building the command. This was deliberate to allow this to work
|
|
28
28
|
# with or without an interface.
|
|
29
|
+
db_shard = Store.db_shard_for_target(target_name, scope: scope)
|
|
29
30
|
ack_topic = "{#{scope}__ACKCMD}TARGET__#{target_name}"
|
|
30
|
-
Topic.update_topic_offsets([ack_topic])
|
|
31
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
31
32
|
decom_id = Topic.write_topic("#{scope}__DECOMINTERFACE__{#{target_name}}",
|
|
32
|
-
{ 'build_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
33
|
+
{ 'build_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
33
34
|
time = Time.now
|
|
34
35
|
while (Time.now - time) < timeout
|
|
35
|
-
Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
|
|
36
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
36
37
|
if msg_hash["id"] == decom_id
|
|
37
38
|
if msg_hash["result"] == "SUCCESS"
|
|
38
39
|
return msg_hash
|
|
@@ -45,19 +46,22 @@ module OpenC3
|
|
|
45
46
|
raise "Timeout of #{timeout}s waiting for cmd ack. Does target '#{target_name}' exist?"
|
|
46
47
|
end
|
|
47
48
|
|
|
48
|
-
def self.inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, timeout: 5, scope:)
|
|
49
|
+
def self.inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, stored: false, timeout: 5, scope:)
|
|
49
50
|
data = {}
|
|
50
51
|
data['target_name'] = target_name.to_s.upcase
|
|
51
52
|
data['packet_name'] = packet_name.to_s.upcase
|
|
52
53
|
data['item_hash'] = item_hash
|
|
53
54
|
data['type'] = type
|
|
55
|
+
|
|
56
|
+
db_shard = Store.db_shard_for_target(target_name, scope: scope)
|
|
57
|
+
data['stored'] = stored
|
|
54
58
|
ack_topic = "{#{scope}__ACKCMD}TARGET__#{target_name}"
|
|
55
|
-
Topic.update_topic_offsets([ack_topic])
|
|
59
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
56
60
|
decom_id = Topic.write_topic("#{scope}__DECOMINTERFACE__{#{target_name}}",
|
|
57
|
-
{ 'inject_tlm' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
61
|
+
{ 'inject_tlm' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
58
62
|
time = Time.now
|
|
59
63
|
while (Time.now - time) < timeout
|
|
60
|
-
Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
|
|
64
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
61
65
|
if msg_hash["id"] == decom_id
|
|
62
66
|
if msg_hash["result"] == "SUCCESS"
|
|
63
67
|
return
|
|
@@ -76,13 +80,14 @@ module OpenC3
|
|
|
76
80
|
data['packet_name'] = packet_name.to_s.upcase
|
|
77
81
|
# DecomMicroservice is listening to the DECOMINTERFACE topic and has
|
|
78
82
|
# the most recent decommed packets including subpackets
|
|
83
|
+
db_shard = Store.db_shard_for_target(target_name, scope: scope)
|
|
79
84
|
ack_topic = "{#{scope}__ACKCMD}TARGET__#{target_name}"
|
|
80
|
-
Topic.update_topic_offsets([ack_topic])
|
|
85
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
81
86
|
decom_id = Topic.write_topic("#{scope}__DECOMINTERFACE__{#{target_name}}",
|
|
82
|
-
{ 'get_tlm_buffer' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
87
|
+
{ 'get_tlm_buffer' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
83
88
|
time = Time.now
|
|
84
89
|
while (Time.now - time) < timeout
|
|
85
|
-
Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
|
|
90
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
86
91
|
if msg_hash["id"] == decom_id
|
|
87
92
|
if msg_hash["result"] == "SUCCESS"
|
|
88
93
|
msg_hash["stored"] = ConfigParser.handle_true_false(msg_hash["stored"])
|
|
@@ -21,6 +21,12 @@ module OpenC3
|
|
|
21
21
|
class InterfaceTopic < Topic
|
|
22
22
|
COMMAND_ACK_TIMEOUT_S = 30
|
|
23
23
|
|
|
24
|
+
# Look up db_shard from Interface
|
|
25
|
+
def self._db_shard_for_interface(interface_name, scope:)
|
|
26
|
+
json = Store.hget("#{scope}__openc3_interfaces", interface_name)
|
|
27
|
+
json ? (JSON.parse(json, allow_nan: true, create_additions: true)['db_shard'] || 0).to_i : 0
|
|
28
|
+
end
|
|
29
|
+
|
|
24
30
|
# Generate a list of topics for this interface. This includes the interface itself
|
|
25
31
|
# and all the targets which are assigned to this interface.
|
|
26
32
|
def self.topics(interface, scope:)
|
|
@@ -33,16 +39,39 @@ module OpenC3
|
|
|
33
39
|
topics
|
|
34
40
|
end
|
|
35
41
|
|
|
36
|
-
def self.receive_commands(interface, scope:)
|
|
42
|
+
def self.receive_commands(interface, scope:, db_shard: 0)
|
|
43
|
+
db_shard = db_shard.to_i
|
|
44
|
+
interface_cmd_topic = "{#{scope}__CMD}INTERFACE__#{interface.name}"
|
|
45
|
+
system_events_topic = "OPENC3__SYSTEM__EVENTS"
|
|
46
|
+
|
|
47
|
+
target_topics = []
|
|
48
|
+
interface.cmd_target_names.each do |target_name|
|
|
49
|
+
target_topics << "{#{scope}__CMD}TARGET__#{target_name}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Group target command topics by db_shard; include interface cmd and system events on db_shard
|
|
53
|
+
db_shard_groups = Topic.group_topics_by_db_shard(target_topics, target_pattern: 'CMD}TARGET__', scope: scope)
|
|
54
|
+
db_shard_groups[db_shard] ||= []
|
|
55
|
+
db_shard_groups[db_shard] << interface_cmd_topic
|
|
56
|
+
db_shard_groups[db_shard] << system_events_topic
|
|
57
|
+
|
|
58
|
+
all_same_db_shard = Topic.all_same_db_shard?(db_shard_groups)
|
|
59
|
+
|
|
37
60
|
while true
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
if all_same_db_shard
|
|
62
|
+
# Fast path: everything on one db_shard, single read
|
|
63
|
+
db_shard = db_shard_groups.keys.first || 0
|
|
64
|
+
Topic.read_topics(db_shard_groups[db_shard], db_shard: db_shard) do |topic, msg_id, msg_hash, redis|
|
|
65
|
+
result = yield topic, msg_id, msg_hash, redis
|
|
66
|
+
Topic.write_ack(topic, result, msg_id, db_shard: db_shard) if result
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
timeout_per_db_shard = [1000 / [db_shard_groups.length, 1].max, 100].max
|
|
70
|
+
db_shard_groups.each do |db_shard, topics|
|
|
71
|
+
Topic.read_topics(topics, nil, timeout_per_db_shard, db_shard: db_shard) do |topic, msg_id, msg_hash, redis|
|
|
72
|
+
result = yield topic, msg_id, msg_hash, redis
|
|
73
|
+
Topic.write_ack(topic, result, msg_id, db_shard: db_shard) if result
|
|
74
|
+
end
|
|
46
75
|
end
|
|
47
76
|
end
|
|
48
77
|
end
|
|
@@ -50,15 +79,16 @@ module OpenC3
|
|
|
50
79
|
|
|
51
80
|
def self.write_raw(interface_name, data, timeout: nil, scope:)
|
|
52
81
|
interface_name = interface_name.upcase
|
|
82
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
53
83
|
|
|
54
84
|
timeout = COMMAND_ACK_TIMEOUT_S unless timeout
|
|
55
85
|
ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
|
|
56
|
-
Topic.update_topic_offsets([ack_topic])
|
|
86
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
57
87
|
|
|
58
|
-
cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'raw' => data }, '*', 100)
|
|
88
|
+
cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'raw' => data }, '*', 100, db_shard: db_shard)
|
|
59
89
|
time = Time.now
|
|
60
90
|
while (Time.now - time) < timeout
|
|
61
|
-
Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
|
|
91
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
62
92
|
if msg_hash["id"] == cmd_id
|
|
63
93
|
if msg_hash["result"] == "SUCCESS"
|
|
64
94
|
return
|
|
@@ -72,61 +102,70 @@ module OpenC3
|
|
|
72
102
|
end
|
|
73
103
|
|
|
74
104
|
def self.connect_interface(interface_name, *interface_params, scope:)
|
|
105
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
75
106
|
if interface_params && !interface_params.empty?
|
|
76
|
-
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'connect' => 'true', 'params' => JSON.generate(interface_params, allow_nan: true) }, '*', 100)
|
|
107
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'connect' => 'true', 'params' => JSON.generate(interface_params, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
77
108
|
else
|
|
78
|
-
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'connect' => 'true' }, '*', 100)
|
|
109
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'connect' => 'true' }, '*', 100, db_shard: db_shard)
|
|
79
110
|
end
|
|
80
111
|
end
|
|
81
112
|
|
|
82
113
|
def self.disconnect_interface(interface_name, scope:)
|
|
83
|
-
|
|
114
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
115
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'disconnect' => 'true' }, '*', 100, db_shard: db_shard)
|
|
84
116
|
end
|
|
85
117
|
|
|
86
118
|
def self.start_raw_logging(interface_name, scope:)
|
|
87
|
-
|
|
119
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
120
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'log_stream' => 'true' }, '*', 100, db_shard: db_shard)
|
|
88
121
|
end
|
|
89
122
|
|
|
90
123
|
def self.stop_raw_logging(interface_name, scope:)
|
|
91
|
-
|
|
124
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
125
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'log_stream' => 'false' }, '*', 100, db_shard: db_shard)
|
|
92
126
|
end
|
|
93
127
|
|
|
94
128
|
def self.shutdown(interface, scope:)
|
|
95
|
-
|
|
129
|
+
db_shard = _db_shard_for_interface(interface.name, scope: scope)
|
|
130
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface.name}", { 'shutdown' => 'true' }, '*', 100, db_shard: db_shard)
|
|
96
131
|
end
|
|
97
132
|
|
|
98
133
|
def self.interface_cmd(interface_name, cmd_name, *cmd_params, scope:)
|
|
134
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
99
135
|
data = {}
|
|
100
136
|
data['cmd_name'] = cmd_name
|
|
101
137
|
data['cmd_params'] = cmd_params
|
|
102
|
-
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'interface_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
138
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'interface_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
103
139
|
end
|
|
104
140
|
|
|
105
141
|
def self.protocol_cmd(interface_name, cmd_name, *cmd_params, read_write: :READ_WRITE, index: -1, scope:)
|
|
142
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
106
143
|
data = {}
|
|
107
144
|
data['cmd_name'] = cmd_name
|
|
108
145
|
data['cmd_params'] = cmd_params
|
|
109
146
|
data['read_write'] = read_write.to_s.upcase
|
|
110
147
|
data['index'] = index
|
|
111
|
-
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'protocol_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
148
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'protocol_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
112
149
|
end
|
|
113
150
|
|
|
114
|
-
def self.inject_tlm(interface_name, target_name, packet_name, item_hash = nil, type: :CONVERTED, timeout: nil, scope:)
|
|
151
|
+
def self.inject_tlm(interface_name, target_name, packet_name, item_hash = nil, type: :CONVERTED, stored: false, timeout: nil, scope:)
|
|
115
152
|
interface_name = interface_name.upcase
|
|
153
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
116
154
|
|
|
117
155
|
timeout = COMMAND_ACK_TIMEOUT_S unless timeout
|
|
118
156
|
ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
|
|
119
|
-
Topic.update_topic_offsets([ack_topic])
|
|
157
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
120
158
|
|
|
121
159
|
data = {}
|
|
122
160
|
data['target_name'] = target_name.to_s.upcase
|
|
123
161
|
data['packet_name'] = packet_name.to_s.upcase
|
|
124
162
|
data['item_hash'] = item_hash
|
|
125
163
|
data['type'] = type
|
|
126
|
-
|
|
164
|
+
data['stored'] = stored
|
|
165
|
+
cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'inject_tlm' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
127
166
|
time = Time.now
|
|
128
167
|
while (Time.now - time) < timeout
|
|
129
|
-
Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
|
|
168
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
130
169
|
if msg_hash["id"] == cmd_id
|
|
131
170
|
if msg_hash["result"] == "SUCCESS"
|
|
132
171
|
return
|
|
@@ -140,34 +179,37 @@ module OpenC3
|
|
|
140
179
|
end
|
|
141
180
|
|
|
142
181
|
def self.interface_target_enable(interface_name, target_name, cmd_only: false, tlm_only: false, scope:)
|
|
182
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
143
183
|
data = {}
|
|
144
184
|
data['target_name'] = target_name.to_s.upcase
|
|
145
185
|
data['cmd_only'] = cmd_only
|
|
146
186
|
data['tlm_only'] = tlm_only
|
|
147
187
|
data['action'] = 'enable'
|
|
148
|
-
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
188
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
149
189
|
end
|
|
150
190
|
|
|
151
191
|
def self.interface_target_disable(interface_name, target_name, cmd_only: false, tlm_only: false, scope:)
|
|
192
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
152
193
|
data = {}
|
|
153
194
|
data['target_name'] = target_name.to_s.upcase
|
|
154
195
|
data['cmd_only'] = cmd_only
|
|
155
196
|
data['tlm_only'] = tlm_only
|
|
156
197
|
data['action'] = 'disable'
|
|
157
|
-
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
198
|
+
Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
158
199
|
end
|
|
159
200
|
|
|
160
201
|
def self.interface_details(interface_name, timeout: nil, scope:)
|
|
161
202
|
interface_name = interface_name.upcase
|
|
203
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
162
204
|
|
|
163
205
|
timeout = COMMAND_ACK_TIMEOUT_S unless timeout
|
|
164
206
|
ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
|
|
165
|
-
Topic.update_topic_offsets([ack_topic])
|
|
207
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
166
208
|
|
|
167
|
-
cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'interface_details' => 'true' }, '*', 100)
|
|
209
|
+
cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'interface_details' => 'true' }, '*', 100, db_shard: db_shard)
|
|
168
210
|
time = Time.now
|
|
169
211
|
while (Time.now - time) < timeout
|
|
170
|
-
Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
|
|
212
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
171
213
|
if msg_hash["id"] == cmd_id
|
|
172
214
|
return JSON.parse(msg_hash["result"], :allow_nan => true, :create_additions => true)
|
|
173
215
|
end
|
|
@@ -25,6 +25,16 @@ module OpenC3
|
|
|
25
25
|
# While this isn't a clean separation of topics (streams) and models (key-value)
|
|
26
26
|
# it helps maintain consistency as the topic and model are linked.
|
|
27
27
|
class LimitsEventTopic < Topic
|
|
28
|
+
# Collect all unique target db_shards from TargetModel
|
|
29
|
+
def self._active_db_shards(scope:)
|
|
30
|
+
db_shards = Set.new([0])
|
|
31
|
+
Store.hgetall("#{scope}__openc3_targets").each do |_name, json|
|
|
32
|
+
parsed = JSON.parse(json, allow_nan: true, create_additions: true)
|
|
33
|
+
db_shards << (parsed['db_shard'] || 0).to_i
|
|
34
|
+
end
|
|
35
|
+
db_shards
|
|
36
|
+
end
|
|
37
|
+
|
|
28
38
|
def self.write(event, scope:)
|
|
29
39
|
case event[:type]
|
|
30
40
|
when :LIMITS_CHANGE
|
|
@@ -81,7 +91,10 @@ module OpenC3
|
|
|
81
91
|
raise "Invalid limits event type '#{event[:type]}'"
|
|
82
92
|
end
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
# Write to all active db_shards so each decom microservice can read limits events inline
|
|
95
|
+
_active_db_shards(scope: scope).each do |db_shard|
|
|
96
|
+
Topic.write_topic("#{scope}__openc3_limits_events", {event: JSON.generate(event, allow_nan: true)}, '*', 1000, db_shard: db_shard)
|
|
97
|
+
end
|
|
85
98
|
end
|
|
86
99
|
|
|
87
100
|
# Remove the JSON encoding to return hashes directly
|
|
@@ -189,51 +202,59 @@ module OpenC3
|
|
|
189
202
|
end
|
|
190
203
|
end
|
|
191
204
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
event['red_low'], event['yellow_low'], event['yellow_high'], event['red_high'],
|
|
213
|
-
event['green_low'], event['green_high'], event['limits_set'], persistence, enabled)
|
|
214
|
-
end
|
|
205
|
+
# Process a single limits event hash and update the local System accordingly.
|
|
206
|
+
# Called inline by DecomMicroservice when reading from the limits events topic.
|
|
207
|
+
def self.process_event(event, telemetry: nil)
|
|
208
|
+
telemetry ||= System.telemetry.all
|
|
209
|
+
case event['type']
|
|
210
|
+
when 'LIMITS_CHANGE'
|
|
211
|
+
# Ignore
|
|
212
|
+
when 'LIMITS_SETTINGS'
|
|
213
|
+
target_name = event['target_name']
|
|
214
|
+
packet_name = event['packet_name']
|
|
215
|
+
item_name = event['item_name']
|
|
216
|
+
target = telemetry[target_name]
|
|
217
|
+
if target
|
|
218
|
+
packet = target[packet_name]
|
|
219
|
+
if packet
|
|
220
|
+
enabled = ConfigParser.handle_true_false_nil(event['enabled'])
|
|
221
|
+
persistence = event['persistence']
|
|
222
|
+
System.limits.set(target_name, packet_name, item_name,
|
|
223
|
+
event['red_low'], event['yellow_low'], event['yellow_high'], event['red_high'],
|
|
224
|
+
event['green_low'], event['green_high'], event['limits_set'], persistence, enabled)
|
|
215
225
|
end
|
|
226
|
+
end
|
|
216
227
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
end
|
|
228
|
+
when 'LIMITS_ENABLE_STATE'
|
|
229
|
+
target_name = event['target_name']
|
|
230
|
+
packet_name = event['packet_name']
|
|
231
|
+
item_name = event['item_name']
|
|
232
|
+
target = telemetry[target_name]
|
|
233
|
+
if target
|
|
234
|
+
packet = target[packet_name]
|
|
235
|
+
if packet
|
|
236
|
+
enabled = ConfigParser.handle_true_false_nil(event['enabled'])
|
|
237
|
+
if enabled
|
|
238
|
+
System.limits.enable(target_name, packet_name, item_name)
|
|
239
|
+
else
|
|
240
|
+
System.limits.disable(target_name, packet_name, item_name)
|
|
231
241
|
end
|
|
232
242
|
end
|
|
233
|
-
|
|
234
|
-
when 'LIMITS_SET'
|
|
235
|
-
System.limits_set = event['set']
|
|
236
243
|
end
|
|
244
|
+
|
|
245
|
+
when 'LIMITS_SET'
|
|
246
|
+
System.limits_set = event['set']
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Update the local system based on limits events (standalone read loop).
|
|
251
|
+
# Still available for non-decom consumers that need to sync limits.
|
|
252
|
+
def self.sync_system_thread_body(scope:, block_ms: nil)
|
|
253
|
+
telemetry = System.telemetry.all
|
|
254
|
+
topics = ["#{scope}__openc3_limits_events"]
|
|
255
|
+
Topic.read_topics(topics, nil, block_ms) do |_topic, _msg_id, event, _redis|
|
|
256
|
+
event = JSON.parse(event['event'], allow_nan: true, create_additions: true)
|
|
257
|
+
process_event(event, telemetry: telemetry)
|
|
237
258
|
end
|
|
238
259
|
end
|
|
239
260
|
end
|