openc3 6.9.1 → 6.10.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 +289 -0
- data/data/config/command_modifiers.yaml +79 -0
- data/data/config/item_modifiers.yaml +5 -0
- data/data/config/parameter_modifiers.yaml +5 -0
- data/data/config/telemetry_modifiers.yaml +79 -0
- data/ext/openc3/ext/packet/packet.c +9 -0
- data/lib/openc3/accessors/accessor.rb +27 -3
- data/lib/openc3/accessors/binary_accessor.rb +21 -4
- data/lib/openc3/accessors/template_accessor.rb +3 -2
- data/lib/openc3/api/cmd_api.rb +7 -3
- data/lib/openc3/api/tlm_api.rb +17 -7
- data/lib/openc3/interfaces/protocols/fixed_protocol.rb +19 -13
- data/lib/openc3/interfaces.rb +6 -4
- data/lib/openc3/io/json_rpc.rb +6 -0
- data/lib/openc3/microservices/decom_microservice.rb +97 -17
- data/lib/openc3/microservices/interface_decom_common.rb +32 -0
- data/lib/openc3/microservices/interface_microservice.rb +12 -80
- data/lib/openc3/microservices/queue_microservice.rb +30 -7
- data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +23 -0
- data/lib/openc3/models/plugin_model.rb +69 -6
- data/lib/openc3/models/queue_model.rb +32 -5
- data/lib/openc3/models/reaction_model.rb +26 -10
- data/lib/openc3/models/target_model.rb +85 -13
- data/lib/openc3/models/trigger_model.rb +1 -1
- data/lib/openc3/packets/commands.rb +33 -7
- data/lib/openc3/packets/packet.rb +75 -71
- data/lib/openc3/packets/packet_config.rb +78 -29
- data/lib/openc3/packets/packet_item.rb +11 -103
- data/lib/openc3/packets/parsers/packet_item_parser.rb +177 -34
- data/lib/openc3/packets/parsers/xtce_converter.rb +2 -2
- data/lib/openc3/packets/structure.rb +29 -21
- data/lib/openc3/packets/structure_item.rb +31 -19
- data/lib/openc3/packets/telemetry.rb +37 -11
- data/lib/openc3/script/script.rb +1 -1
- data/lib/openc3/script/suite_results.rb +2 -2
- data/lib/openc3/subpacketizers/subpacketizer.rb +18 -0
- data/lib/openc3/system/system.rb +3 -3
- data/lib/openc3/system/target.rb +3 -32
- data/lib/openc3/tools/table_manager/table_config.rb +9 -1
- data/lib/openc3/tools/table_manager/table_item_parser.rb +2 -2
- data/lib/openc3/top_level.rb +45 -19
- data/lib/openc3/topics/decom_interface_topic.rb +31 -0
- data/lib/openc3/utilities/authentication.rb +25 -6
- data/lib/openc3/utilities/cli_generator.rb +347 -3
- data/lib/openc3/utilities/env_helper.rb +10 -0
- data/lib/openc3/utilities/logger.rb +7 -11
- data/lib/openc3/version.rb +6 -6
- data/tasks/spec.rake +2 -1
- data/templates/command_validator/command_validator.py +49 -0
- data/templates/command_validator/command_validator.rb +54 -0
- data/templates/tool_angular/package.json +48 -2
- data/templates/tool_react/package.json +51 -1
- data/templates/tool_svelte/package.json +48 -1
- data/templates/tool_vue/package.json +36 -3
- data/templates/widget/package.json +28 -2
- metadata +8 -5
- data/templates/tool_vue/.browserslistrc +0 -16
- data/templates/widget/.browserslistrc +0 -16
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
require 'openc3/microservices/microservice'
|
|
20
20
|
require 'openc3/topics/queue_topic'
|
|
21
|
+
require 'openc3/models/queue_model'
|
|
21
22
|
require 'openc3/utilities/authentication'
|
|
22
23
|
require 'openc3/api/api'
|
|
23
24
|
|
|
@@ -66,7 +67,22 @@ module OpenC3
|
|
|
66
67
|
# OPENC3_DEFAULT_QUEUE is set because commands would be re-queued to the default queue
|
|
67
68
|
# NOTE: cmd() via script rescues hazardous errors and calls prompt_for_hazardous()
|
|
68
69
|
# but we've overridden it to always return true and go straight to cmd_no_hazardous_check()
|
|
69
|
-
|
|
70
|
+
|
|
71
|
+
# Support both new format (target_name, cmd_name, cmd_params) and legacy format (command string)
|
|
72
|
+
if command['target_name'] && command['cmd_name']
|
|
73
|
+
# New format: use 3-parameter cmd() method
|
|
74
|
+
if command['cmd_params']
|
|
75
|
+
cmd_params = JSON.parse(command['cmd_params'], allow_nan: true, create_additions: true)
|
|
76
|
+
else
|
|
77
|
+
cmd_params = {}
|
|
78
|
+
end
|
|
79
|
+
cmd(command['target_name'], command['cmd_name'], cmd_params, queue: false, scope: @scope)
|
|
80
|
+
elsif command['value']
|
|
81
|
+
# Legacy format: use single string parameter for backwards compatibility
|
|
82
|
+
cmd(command['value'], queue: false, scope: @scope)
|
|
83
|
+
else
|
|
84
|
+
@logger.error "QueueProcessor: Invalid command format, missing required fields"
|
|
85
|
+
end
|
|
70
86
|
end
|
|
71
87
|
rescue StandardError => e
|
|
72
88
|
@logger.error "QueueProcessor failed to process command from queue #{@name}\n#{e.message}"
|
|
@@ -90,15 +106,22 @@ module OpenC3
|
|
|
90
106
|
@queue_name = @name.split('__')[2]
|
|
91
107
|
|
|
92
108
|
initial_state = 'HOLD'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
# See if the queue already exists to get its state
|
|
110
|
+
queue = OpenC3::QueueModel.get(name: @queue_name, scope: @scope)
|
|
111
|
+
if queue
|
|
112
|
+
initial_state = queue['state']
|
|
113
|
+
else
|
|
114
|
+
(@config['options'] || []).each do |option|
|
|
115
|
+
case option[0].upcase
|
|
116
|
+
when 'QUEUE_STATE'
|
|
117
|
+
initial_state = option[1]
|
|
118
|
+
else
|
|
119
|
+
@logger.error("Unknown option passed to microservice #{@name}: #{option}")
|
|
120
|
+
end
|
|
99
121
|
end
|
|
100
122
|
end
|
|
101
123
|
|
|
124
|
+
@logger.info "Creating QueueMicroservice in scope #{@scope} for queue #{@queue_name} with initial state #{initial_state}"
|
|
102
125
|
@processor = QueueProcessor.new(name: @queue_name, state: initial_state, logger: @logger, scope: @scope)
|
|
103
126
|
@processor_thread = nil
|
|
104
127
|
@read_topic = true
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'openc3/utilities/migration'
|
|
2
|
+
require 'openc3/models/scope_model'
|
|
3
|
+
require 'openc3/models/microservice_model'
|
|
4
|
+
|
|
5
|
+
module OpenC3
|
|
6
|
+
class RemoveUniqueId < Migration
|
|
7
|
+
def self.run
|
|
8
|
+
ScopeModel.get_all_models(scope: nil).each do |scope, scope_model|
|
|
9
|
+
target_models = TargetModel.all(scope: scope)
|
|
10
|
+
target_models.each do |name, target_model|
|
|
11
|
+
target_model.delete("cmd_unique_id_mode")
|
|
12
|
+
target_model.delete("tlm_unique_id_mode")
|
|
13
|
+
model = TargetModel.from_json(target_model, scope: scope)
|
|
14
|
+
model.update()
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
unless ENV['OPENC3_NO_MIGRATE']
|
|
22
|
+
OpenC3::RemoveUniqueId.run
|
|
23
|
+
end
|
|
@@ -41,6 +41,8 @@ require 'tempfile'
|
|
|
41
41
|
require 'fileutils'
|
|
42
42
|
|
|
43
43
|
module OpenC3
|
|
44
|
+
class EmptyGemFileError < StandardError; end
|
|
45
|
+
|
|
44
46
|
# Represents a OpenC3 plugin that can consist of targets, interfaces, routers
|
|
45
47
|
# microservices and tools. The PluginModel installs all these pieces as well
|
|
46
48
|
# as destroys them all when the plugin is removed.
|
|
@@ -56,6 +58,13 @@ module OpenC3
|
|
|
56
58
|
attr_accessor :plugin_txt_lines
|
|
57
59
|
attr_accessor :needs_dependencies
|
|
58
60
|
attr_accessor :store_id
|
|
61
|
+
attr_accessor :title
|
|
62
|
+
attr_accessor :description
|
|
63
|
+
attr_accessor :licenses
|
|
64
|
+
attr_accessor :homepage
|
|
65
|
+
attr_accessor :repository
|
|
66
|
+
attr_accessor :keywords
|
|
67
|
+
attr_accessor :img_path
|
|
59
68
|
|
|
60
69
|
# NOTE: The following three class methods are used by the ModelController
|
|
61
70
|
# and are reimplemented to enable various Model class methods to work
|
|
@@ -80,6 +89,10 @@ module OpenC3
|
|
|
80
89
|
tf = nil
|
|
81
90
|
begin
|
|
82
91
|
if File.exist?(gem_file_path)
|
|
92
|
+
if File.zero?(gem_file_path)
|
|
93
|
+
raise EmptyGemFileError, "Gem file is empty: #{gem_file_path}"
|
|
94
|
+
end
|
|
95
|
+
|
|
83
96
|
# Load gem to internal gem server
|
|
84
97
|
OpenC3::GemModel.put(gem_file_path, gem_install: false, scope: scope) unless validate_only
|
|
85
98
|
else
|
|
@@ -179,11 +192,31 @@ module OpenC3
|
|
|
179
192
|
raise "Invalid screen filename: #{filename}. Screen filenames must be lowercase."
|
|
180
193
|
end
|
|
181
194
|
end
|
|
195
|
+
|
|
196
|
+
# Process app store metadata
|
|
197
|
+
plugin_model.title = pkg.spec.metadata['openc3_store_title'] || pkg.spec.summary.strip
|
|
198
|
+
plugin_model.description = pkg.spec.metadata['openc3_store_description'] || pkg.spec.description.strip
|
|
199
|
+
plugin_model.licenses = pkg.spec.licenses
|
|
200
|
+
plugin_model.homepage = pkg.spec.homepage
|
|
201
|
+
plugin_model.repository = pkg.spec.metadata['source_code_uri'] # this key because it's in the official gemspec examples
|
|
202
|
+
plugin_model.keywords = pkg.spec.metadata['openc3_store_keywords']&.split(/, ?/)
|
|
203
|
+
img_path = pkg.spec.metadata['openc3_store_image']
|
|
204
|
+
unless img_path
|
|
205
|
+
default_img_path = 'public/store_img.png'
|
|
206
|
+
full_default_path = File.join(gem_path, default_img_path)
|
|
207
|
+
img_path = default_img_path if File.exist? full_default_path
|
|
208
|
+
end
|
|
209
|
+
plugin_model.img_path = File.join('gems', gem_name.split(".gem")[0], img_path) if img_path # convert this filesystem path to volumes mount path
|
|
210
|
+
plugin_model.update() unless validate_only
|
|
211
|
+
|
|
182
212
|
needs_dependencies = pkg.spec.runtime_dependencies.length > 0
|
|
183
213
|
needs_dependencies = true if Dir.exist?(File.join(gem_path, 'lib'))
|
|
184
214
|
|
|
185
|
-
# Handle python requirements.txt
|
|
186
|
-
|
|
215
|
+
# Handle python dependencies (pyproject.toml or requirements.txt)
|
|
216
|
+
pyproject_path = File.join(gem_path, 'pyproject.toml')
|
|
217
|
+
requirements_path = File.join(gem_path, 'requirements.txt')
|
|
218
|
+
|
|
219
|
+
if File.exist?(pyproject_path) || File.exist?(requirements_path)
|
|
187
220
|
begin
|
|
188
221
|
pypi_url = get_setting('pypi_url', scope: scope)
|
|
189
222
|
if pypi_url
|
|
@@ -202,11 +235,20 @@ module OpenC3
|
|
|
202
235
|
end
|
|
203
236
|
end
|
|
204
237
|
unless validate_only
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
238
|
+
if File.exist?(pyproject_path)
|
|
239
|
+
Logger.info "Installing python packages from pyproject.toml with pypi_url=#{pypi_url}"
|
|
240
|
+
if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
241
|
+
pip_args = "--no-warn-script-location -i #{pypi_url} #{gem_path}"
|
|
242
|
+
else
|
|
243
|
+
pip_args = "--no-warn-script-location -i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} #{gem_path}"
|
|
244
|
+
end
|
|
208
245
|
else
|
|
209
|
-
|
|
246
|
+
Logger.info "Installing python packages from requirements.txt with pypi_url=#{pypi_url}"
|
|
247
|
+
if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
248
|
+
pip_args = "--no-warn-script-location -i #{pypi_url} -r #{requirements_path}"
|
|
249
|
+
else
|
|
250
|
+
pip_args = "--no-warn-script-location -i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} -r #{requirements_path}"
|
|
251
|
+
end
|
|
210
252
|
end
|
|
211
253
|
puts `/openc3/bin/pipinstall #{pip_args}`
|
|
212
254
|
end
|
|
@@ -302,6 +344,13 @@ module OpenC3
|
|
|
302
344
|
plugin_txt_lines: [],
|
|
303
345
|
needs_dependencies: false,
|
|
304
346
|
store_id: nil,
|
|
347
|
+
title: nil,
|
|
348
|
+
description: nil,
|
|
349
|
+
keywords: nil,
|
|
350
|
+
licenses: nil,
|
|
351
|
+
homepage: nil,
|
|
352
|
+
repository: nil,
|
|
353
|
+
img_path: nil,
|
|
305
354
|
updated_at: nil,
|
|
306
355
|
scope:
|
|
307
356
|
)
|
|
@@ -310,6 +359,13 @@ module OpenC3
|
|
|
310
359
|
@plugin_txt_lines = plugin_txt_lines
|
|
311
360
|
@needs_dependencies = ConfigParser.handle_true_false(needs_dependencies)
|
|
312
361
|
@store_id = store_id
|
|
362
|
+
@title = title
|
|
363
|
+
@description = description
|
|
364
|
+
@keywords = keywords
|
|
365
|
+
@licenses = licenses
|
|
366
|
+
@homepage = homepage
|
|
367
|
+
@repository = repository
|
|
368
|
+
@img_path = img_path
|
|
313
369
|
end
|
|
314
370
|
|
|
315
371
|
def create(update: false, force: false, queued: false)
|
|
@@ -329,6 +385,13 @@ module OpenC3
|
|
|
329
385
|
'plugin_txt_lines' => @plugin_txt_lines,
|
|
330
386
|
'needs_dependencies' => @needs_dependencies,
|
|
331
387
|
'store_id' => @store_id,
|
|
388
|
+
'title' => @title,
|
|
389
|
+
'description' => @description,
|
|
390
|
+
'keywords' => @keywords,
|
|
391
|
+
'licenses' => @licenses,
|
|
392
|
+
'homepage' => @homepage,
|
|
393
|
+
'repository' => @repository,
|
|
394
|
+
'img_path' => @img_path,
|
|
332
395
|
'updated_at' => @updated_at
|
|
333
396
|
}
|
|
334
397
|
end
|
|
@@ -20,6 +20,7 @@ require 'openc3/models/model'
|
|
|
20
20
|
require 'openc3/models/microservice_model'
|
|
21
21
|
require 'openc3/topics/queue_topic'
|
|
22
22
|
require 'openc3/utilities/logger'
|
|
23
|
+
require 'openc3/io/json_rpc'
|
|
23
24
|
|
|
24
25
|
module OpenC3
|
|
25
26
|
class QueueError < StandardError; end
|
|
@@ -44,7 +45,7 @@ module OpenC3
|
|
|
44
45
|
end
|
|
45
46
|
# END NOTE
|
|
46
47
|
|
|
47
|
-
def self.queue_command(name, command
|
|
48
|
+
def self.queue_command(name, command: nil, target_name: nil, cmd_name: nil, cmd_params: nil, username:, scope:)
|
|
48
49
|
model = get_model(name: name, scope: scope)
|
|
49
50
|
raise QueueError, "Queue '#{name}' not found in scope '#{scope}'" unless model
|
|
50
51
|
|
|
@@ -55,10 +56,26 @@ module OpenC3
|
|
|
55
56
|
else
|
|
56
57
|
id = result[0][1].to_f + 1
|
|
57
58
|
end
|
|
58
|
-
|
|
59
|
+
|
|
60
|
+
# Build command data with support for both formats
|
|
61
|
+
command_data = { username: username, timestamp: Time.now.to_nsec_from_epoch }
|
|
62
|
+
if target_name && cmd_name
|
|
63
|
+
# New format: store target_name, cmd_name, and cmd_params separately
|
|
64
|
+
command_data[:target_name] = target_name
|
|
65
|
+
command_data[:cmd_name] = cmd_name
|
|
66
|
+
command_data[:cmd_params] = JSON.generate(cmd_params.as_json, allow_nan: true) if cmd_params
|
|
67
|
+
elsif command
|
|
68
|
+
# Legacy format: store command string for backwards compatibility
|
|
69
|
+
command_data[:value] = command
|
|
70
|
+
else
|
|
71
|
+
raise QueueError, "Must provide either command string or target_name/cmd_name parameters"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Store.zadd("#{scope}:#{name}", id, command_data.to_json)
|
|
59
75
|
model.notify(kind: 'command')
|
|
60
76
|
else
|
|
61
|
-
|
|
77
|
+
error_msg = command || "#{target_name} #{cmd_name}"
|
|
78
|
+
raise QueueError, "Queue '#{name}' is disabled. Command '#{error_msg}' not queued."
|
|
62
79
|
end
|
|
63
80
|
end
|
|
64
81
|
|
|
@@ -105,7 +122,12 @@ module OpenC3
|
|
|
105
122
|
|
|
106
123
|
def insert_command(id, command_data)
|
|
107
124
|
if @state == 'DISABLE'
|
|
108
|
-
|
|
125
|
+
if command_data['value']
|
|
126
|
+
command_name = command_data['value']
|
|
127
|
+
else
|
|
128
|
+
command_name = "#{command_data['target_name']} #{command_data['cmd_name']}"
|
|
129
|
+
end
|
|
130
|
+
raise QueueError, "Queue '#{@name}' is disabled. Command '#{command_name}' not queued."
|
|
109
131
|
end
|
|
110
132
|
|
|
111
133
|
unless id
|
|
@@ -116,6 +138,11 @@ module OpenC3
|
|
|
116
138
|
id = result[0][1].to_f + 1
|
|
117
139
|
end
|
|
118
140
|
end
|
|
141
|
+
|
|
142
|
+
# Convert cmd_params values to JSON-safe format if present
|
|
143
|
+
if command_data['cmd_params']
|
|
144
|
+
command_data['cmd_params'] = JSON.generate(command_data['cmd_params'].as_json, allow_nan: true)
|
|
145
|
+
end
|
|
119
146
|
Store.zadd("#{@scope}:#{@name}", id, command_data.to_json)
|
|
120
147
|
notify(kind: 'command')
|
|
121
148
|
end
|
|
@@ -163,7 +190,7 @@ module OpenC3
|
|
|
163
190
|
else
|
|
164
191
|
score = result[0][1]
|
|
165
192
|
Store.zremrangebyscore("#{@scope}:#{@name}", score, score)
|
|
166
|
-
command_data = JSON.parse(result[0][0])
|
|
193
|
+
command_data = JSON.parse(result[0][0], allow_nan: true)
|
|
167
194
|
command_data['id'] = score.to_f
|
|
168
195
|
notify(kind: 'command')
|
|
169
196
|
return command_data
|
|
@@ -143,7 +143,7 @@ module OpenC3
|
|
|
143
143
|
unless triggers.is_a?(Array)
|
|
144
144
|
raise ReactionInputError.new "invalid triggers, must be array of hashes: #{triggers}"
|
|
145
145
|
end
|
|
146
|
-
trigger_hash =
|
|
146
|
+
trigger_hash = {}
|
|
147
147
|
triggers.each do | trigger |
|
|
148
148
|
unless trigger.is_a?(Hash)
|
|
149
149
|
raise ReactionInputError.new "invalid trigger, must be hash: #{trigger}"
|
|
@@ -183,20 +183,17 @@ module OpenC3
|
|
|
183
183
|
end
|
|
184
184
|
|
|
185
185
|
def verify_triggers
|
|
186
|
-
|
|
186
|
+
if @triggers.empty?
|
|
187
|
+
raise ReactionInputError.new "reaction must contain at least one valid trigger: #{@triggers}"
|
|
188
|
+
end
|
|
189
|
+
|
|
187
190
|
@triggers.each do | trigger |
|
|
188
191
|
model = TriggerModel.get(name: trigger['name'], group: trigger['group'], scope: @scope)
|
|
189
192
|
if model.nil?
|
|
190
193
|
raise ReactionInputError.new "failed to find trigger: #{trigger}"
|
|
191
194
|
end
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if trigger_models.empty?
|
|
195
|
-
raise ReactionInputError.new "reaction must contain at least one valid trigger: #{@triggers}"
|
|
196
|
-
end
|
|
197
|
-
trigger_models.each do | trigger_model |
|
|
198
|
-
trigger_model.update_dependents(dependent: @name)
|
|
199
|
-
trigger_model.update()
|
|
195
|
+
model.update_dependents(dependent: @name)
|
|
196
|
+
model.update()
|
|
200
197
|
end
|
|
201
198
|
end
|
|
202
199
|
|
|
@@ -211,6 +208,25 @@ module OpenC3
|
|
|
211
208
|
end
|
|
212
209
|
|
|
213
210
|
def update
|
|
211
|
+
old_reaction = ReactionModel.get(name: @name, scope: @scope)
|
|
212
|
+
|
|
213
|
+
if old_reaction
|
|
214
|
+
# Find triggers that are being removed (in old but not in new)
|
|
215
|
+
old_trigger_keys = old_reaction.triggers.map { |t| "#{t['group']}:#{t['name']}" }
|
|
216
|
+
new_trigger_keys = @triggers.map { |t| "#{t['group']}:#{t['name']}" }
|
|
217
|
+
removed_trigger_keys = old_trigger_keys - new_trigger_keys
|
|
218
|
+
|
|
219
|
+
# Remove this reaction from old triggers' dependents
|
|
220
|
+
removed_trigger_keys.each do |trigger_key|
|
|
221
|
+
group, name = trigger_key.split(':', 2)
|
|
222
|
+
trigger_model = TriggerModel.get(name: name, group: group, scope: @scope)
|
|
223
|
+
if trigger_model
|
|
224
|
+
trigger_model.update_dependents(dependent: @name, remove: true)
|
|
225
|
+
trigger_model.update()
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
214
230
|
verify_triggers()
|
|
215
231
|
@updated_at = Time.now.to_nsec_from_epoch
|
|
216
232
|
Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
|
|
@@ -54,6 +54,9 @@ module OpenC3
|
|
|
54
54
|
ERB_EXTENSIONS = %w(.txt .rb .py .json .yaml .yml)
|
|
55
55
|
ITEM_MAP_CACHE_TIMEOUT = 10.0
|
|
56
56
|
@@item_map_cache = {}
|
|
57
|
+
@@sync_packet_count_data = {}
|
|
58
|
+
@@sync_packet_count_time = nil
|
|
59
|
+
@@sync_packet_count_delay_seconds = 1.0 # Sync packet counts every second
|
|
57
60
|
|
|
58
61
|
attr_accessor :folder_name
|
|
59
62
|
attr_accessor :requires
|
|
@@ -61,8 +64,6 @@ module OpenC3
|
|
|
61
64
|
attr_accessor :ignored_items
|
|
62
65
|
attr_accessor :limits_groups
|
|
63
66
|
attr_accessor :cmd_tlm_files
|
|
64
|
-
attr_accessor :cmd_unique_id_mode
|
|
65
|
-
attr_accessor :tlm_unique_id_mode
|
|
66
67
|
attr_accessor :id
|
|
67
68
|
attr_accessor :cmd_buffer_depth
|
|
68
69
|
attr_accessor :cmd_log_cycle_time
|
|
@@ -166,10 +167,14 @@ module OpenC3
|
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
def self.download(target_name, scope:)
|
|
170
|
+
# Validate target_name to not allow directory traversal
|
|
171
|
+
if target_name.include?('..') || target_name.include?('/') || target_name.include?('\\')
|
|
172
|
+
raise ArgumentError, "Invalid target_name: #{target_name.inspect}"
|
|
173
|
+
end
|
|
169
174
|
tmp_dir = Dir.mktmpdir
|
|
170
175
|
zip_filename = File.join(tmp_dir, "#{target_name}.zip")
|
|
171
176
|
Zip.continue_on_exists_proc = true
|
|
172
|
-
zip = Zip::File.open(zip_filename,
|
|
177
|
+
zip = Zip::File.open(zip_filename, create: true)
|
|
173
178
|
|
|
174
179
|
if ENV['OPENC3_LOCAL_MODE']
|
|
175
180
|
OpenC3::LocalMode.zip_target(target_name, zip, scope: scope)
|
|
@@ -347,8 +352,6 @@ module OpenC3
|
|
|
347
352
|
ignored_items: [],
|
|
348
353
|
limits_groups: [],
|
|
349
354
|
cmd_tlm_files: [],
|
|
350
|
-
cmd_unique_id_mode: false,
|
|
351
|
-
tlm_unique_id_mode: false,
|
|
352
355
|
id: nil,
|
|
353
356
|
updated_at: nil,
|
|
354
357
|
plugin: nil,
|
|
@@ -398,8 +401,6 @@ module OpenC3
|
|
|
398
401
|
@ignored_items = ignored_items
|
|
399
402
|
@limits_groups = limits_groups
|
|
400
403
|
@cmd_tlm_files = cmd_tlm_files
|
|
401
|
-
@cmd_unique_id_mode = cmd_unique_id_mode
|
|
402
|
-
@tlm_unique_id_mode = tlm_unique_id_mode
|
|
403
404
|
@id = id
|
|
404
405
|
@cmd_buffer_depth = cmd_buffer_depth
|
|
405
406
|
@cmd_log_cycle_time = cmd_log_cycle_time
|
|
@@ -438,8 +439,6 @@ module OpenC3
|
|
|
438
439
|
'ignored_items' => @ignored_items,
|
|
439
440
|
'limits_groups' => @limits_groups,
|
|
440
441
|
'cmd_tlm_files' => @cmd_tlm_files,
|
|
441
|
-
'cmd_unique_id_mode' => @cmd_unique_id_mode,
|
|
442
|
-
'tlm_unique_id_mode' => @tlm_unique_id_mode,
|
|
443
442
|
'id' => @id,
|
|
444
443
|
'updated_at' => @updated_at,
|
|
445
444
|
'plugin' => @plugin,
|
|
@@ -756,7 +755,7 @@ module OpenC3
|
|
|
756
755
|
prefix = File.dirname(target_folder) + '/'
|
|
757
756
|
output_file = File.join(temp_dir, @name + '_' + @id + '.zip')
|
|
758
757
|
Zip.continue_on_exists_proc = true
|
|
759
|
-
Zip::File.open(output_file,
|
|
758
|
+
Zip::File.open(output_file, create: true) do |zipfile|
|
|
760
759
|
target_files.each do |target_file|
|
|
761
760
|
zip_file_path = target_file.delete_prefix(prefix)
|
|
762
761
|
if File.directory?(target_file)
|
|
@@ -786,8 +785,6 @@ module OpenC3
|
|
|
786
785
|
@ignored_parameters = target.ignored_parameters
|
|
787
786
|
@ignored_items = target.ignored_items
|
|
788
787
|
@cmd_tlm_files = target.cmd_tlm_files
|
|
789
|
-
@cmd_unique_id_mode = target.cmd_unique_id_mode
|
|
790
|
-
@tlm_unique_id_mode = target.tlm_unique_id_mode
|
|
791
788
|
@limits_groups = system.limits.groups.keys
|
|
792
789
|
update()
|
|
793
790
|
end
|
|
@@ -807,7 +804,7 @@ module OpenC3
|
|
|
807
804
|
Logger.error("Invalid text present in #{target_name} #{packet_name} tlm packet")
|
|
808
805
|
raise e
|
|
809
806
|
end
|
|
810
|
-
json_hash =
|
|
807
|
+
json_hash = {}
|
|
811
808
|
packet.sorted_items.each do |item|
|
|
812
809
|
json_hash[item.name] = nil
|
|
813
810
|
TargetModel.add_to_target_allitems_list(target_name, item.name, scope: @scope)
|
|
@@ -1361,6 +1358,81 @@ module OpenC3
|
|
|
1361
1358
|
return counts
|
|
1362
1359
|
end
|
|
1363
1360
|
|
|
1361
|
+
def self.sync_packet_count_delay_seconds=(value)
|
|
1362
|
+
@@sync_packet_count_delay_seconds = value
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
def self.init_tlm_packet_counts(tlm_target_names, scope:)
|
|
1366
|
+
@@sync_packet_count_time = Time.now
|
|
1367
|
+
|
|
1368
|
+
# Get all the packet counts with the global counters
|
|
1369
|
+
tlm_target_names.each do |target_name|
|
|
1370
|
+
get_all_telemetry_counts(target_name, scope: scope).each do |packet_name, count|
|
|
1371
|
+
update_packet = System.telemetry.packet(target_name, packet_name)
|
|
1372
|
+
update_packet.received_count = count.to_i
|
|
1373
|
+
end
|
|
1374
|
+
end
|
|
1375
|
+
get_all_telemetry_counts('UNKNOWN', scope: scope).each do |packet_name, count|
|
|
1376
|
+
update_packet = System.telemetry.packet('UNKNOWN', packet_name)
|
|
1377
|
+
update_packet.received_count = count.to_i
|
|
1378
|
+
end
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
def self.sync_tlm_packet_counts(packet, tlm_target_names, scope:)
|
|
1382
|
+
if @@sync_packet_count_delay_seconds <= 0 or $openc3_redis_cluster
|
|
1383
|
+
# Perfect but slow method
|
|
1384
|
+
packet.received_count = increment_telemetry_count(packet.target_name, packet.packet_name, 1, scope: scope)
|
|
1385
|
+
else
|
|
1386
|
+
# Eventually consistent method
|
|
1387
|
+
# Only sync every period (default 1 second) to avoid hammering Redis
|
|
1388
|
+
# This is a trade off between speed and accuracy
|
|
1389
|
+
# The packet count is eventually consistent
|
|
1390
|
+
@@sync_packet_count_data[packet.target_name] ||= {}
|
|
1391
|
+
@@sync_packet_count_data[packet.target_name][packet.packet_name] ||= 0
|
|
1392
|
+
@@sync_packet_count_data[packet.target_name][packet.packet_name] += 1
|
|
1393
|
+
|
|
1394
|
+
# Ensures counters change between syncs
|
|
1395
|
+
update_packet = System.telemetry.packet(packet.target_name, packet.packet_name)
|
|
1396
|
+
update_packet.received_count += 1
|
|
1397
|
+
packet.received_count = update_packet.received_count
|
|
1398
|
+
|
|
1399
|
+
# Check if we need to sync the packet counts
|
|
1400
|
+
if @@sync_packet_count_time.nil? or (Time.now - @@sync_packet_count_time) > @@sync_packet_count_delay_seconds
|
|
1401
|
+
@@sync_packet_count_time = Time.now
|
|
1402
|
+
|
|
1403
|
+
inc_count = 0
|
|
1404
|
+
# Use pipeline to make this one transaction
|
|
1405
|
+
result = Store.redis_pool.pipelined do
|
|
1406
|
+
# Increment global counters for packets received
|
|
1407
|
+
@@sync_packet_count_data.each do |target_name, packet_data|
|
|
1408
|
+
packet_data.each do |packet_name, count|
|
|
1409
|
+
increment_telemetry_count(target_name, packet_name, count, scope: scope)
|
|
1410
|
+
inc_count += 1
|
|
1411
|
+
end
|
|
1412
|
+
end
|
|
1413
|
+
@@sync_packet_count_data = {}
|
|
1414
|
+
|
|
1415
|
+
# Get all the packet counts with the global counters
|
|
1416
|
+
tlm_target_names.each do |target_name|
|
|
1417
|
+
get_all_telemetry_counts(target_name, scope: scope)
|
|
1418
|
+
end
|
|
1419
|
+
get_all_telemetry_counts('UNKNOWN', scope: scope)
|
|
1420
|
+
end
|
|
1421
|
+
tlm_target_names.each do |target_name|
|
|
1422
|
+
result[inc_count].each do |packet_name, count|
|
|
1423
|
+
update_packet = System.telemetry.packet(target_name, packet_name)
|
|
1424
|
+
update_packet.received_count = count.to_i
|
|
1425
|
+
end
|
|
1426
|
+
inc_count += 1
|
|
1427
|
+
end
|
|
1428
|
+
result[inc_count].each do |packet_name, count|
|
|
1429
|
+
update_packet = System.telemetry.packet('UNKNOWN', packet_name)
|
|
1430
|
+
update_packet.received_count = count.to_i
|
|
1431
|
+
end
|
|
1432
|
+
end
|
|
1433
|
+
end
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1364
1436
|
def self.increment_command_count(target_name, packet_name, count, scope:)
|
|
1365
1437
|
result = Store.hincrby("#{scope}__COMMANDCNTS__{#{target_name}}", packet_name, count)
|
|
1366
1438
|
if String === result
|
|
@@ -105,7 +105,7 @@ module OpenC3
|
|
|
105
105
|
#
|
|
106
106
|
# @param (see #identify_tlm!)
|
|
107
107
|
# @return (see #identify_tlm!)
|
|
108
|
-
def identify(packet_data, target_names = nil)
|
|
108
|
+
def identify(packet_data, target_names = nil, subpackets: false)
|
|
109
109
|
identified_packet = nil
|
|
110
110
|
|
|
111
111
|
target_names = target_names() unless target_names
|
|
@@ -120,21 +120,39 @@ module OpenC3
|
|
|
120
120
|
next
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
if target and target.cmd_unique_id_mode
|
|
123
|
+
if (not subpackets and System.commands.cmd_unique_id_mode(target_name)) or (subpackets and System.commands.cmd_subpacket_unique_id_mode(target_name))
|
|
125
124
|
# Iterate through the packets and see if any represent the buffer
|
|
126
125
|
target_packets.each do |_packet_name, packet|
|
|
127
|
-
if
|
|
126
|
+
if subpackets
|
|
127
|
+
next unless packet.subpacket
|
|
128
|
+
else
|
|
129
|
+
next if packet.subpacket
|
|
130
|
+
end
|
|
131
|
+
if packet.identify?(packet_data) # Handles virtual
|
|
128
132
|
identified_packet = packet
|
|
129
133
|
break
|
|
130
134
|
end
|
|
131
135
|
end
|
|
132
136
|
else
|
|
133
137
|
# Do a hash lookup to quickly identify the packet
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
packet = nil
|
|
139
|
+
target_packets.each do |_packet_name, target_packet|
|
|
140
|
+
next if target_packet.virtual
|
|
141
|
+
if subpackets
|
|
142
|
+
next unless target_packet.subpacket
|
|
143
|
+
else
|
|
144
|
+
next if target_packet.subpacket
|
|
145
|
+
end
|
|
146
|
+
packet = target_packet
|
|
147
|
+
break
|
|
148
|
+
end
|
|
149
|
+
if packet
|
|
136
150
|
key = packet.read_id_values(packet_data)
|
|
137
|
-
|
|
151
|
+
if subpackets
|
|
152
|
+
hash = @config.cmd_subpacket_id_value_hash[target_name]
|
|
153
|
+
else
|
|
154
|
+
hash = @config.cmd_id_value_hash[target_name]
|
|
155
|
+
end
|
|
138
156
|
identified_packet = hash[key]
|
|
139
157
|
identified_packet = hash['CATCHALL'.freeze] unless identified_packet
|
|
140
158
|
end
|
|
@@ -264,6 +282,14 @@ module OpenC3
|
|
|
264
282
|
@config.dynamic_add_packet(packet, :COMMAND, affect_ids: affect_ids)
|
|
265
283
|
end
|
|
266
284
|
|
|
285
|
+
def cmd_unique_id_mode(target_name)
|
|
286
|
+
return @config.cmd_unique_id_mode[target_name.upcase]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def cmd_subpacket_unique_id_mode(target_name)
|
|
290
|
+
return @config.cmd_subpacket_unique_id_mode[target_name.upcase]
|
|
291
|
+
end
|
|
292
|
+
|
|
267
293
|
protected
|
|
268
294
|
|
|
269
295
|
def set_parameters(command, params, range_checking)
|