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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +289 -0
  3. data/data/config/command_modifiers.yaml +79 -0
  4. data/data/config/item_modifiers.yaml +5 -0
  5. data/data/config/parameter_modifiers.yaml +5 -0
  6. data/data/config/telemetry_modifiers.yaml +79 -0
  7. data/ext/openc3/ext/packet/packet.c +9 -0
  8. data/lib/openc3/accessors/accessor.rb +27 -3
  9. data/lib/openc3/accessors/binary_accessor.rb +21 -4
  10. data/lib/openc3/accessors/template_accessor.rb +3 -2
  11. data/lib/openc3/api/cmd_api.rb +7 -3
  12. data/lib/openc3/api/tlm_api.rb +17 -7
  13. data/lib/openc3/interfaces/protocols/fixed_protocol.rb +19 -13
  14. data/lib/openc3/interfaces.rb +6 -4
  15. data/lib/openc3/io/json_rpc.rb +6 -0
  16. data/lib/openc3/microservices/decom_microservice.rb +97 -17
  17. data/lib/openc3/microservices/interface_decom_common.rb +32 -0
  18. data/lib/openc3/microservices/interface_microservice.rb +12 -80
  19. data/lib/openc3/microservices/queue_microservice.rb +30 -7
  20. data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +23 -0
  21. data/lib/openc3/models/plugin_model.rb +69 -6
  22. data/lib/openc3/models/queue_model.rb +32 -5
  23. data/lib/openc3/models/reaction_model.rb +26 -10
  24. data/lib/openc3/models/target_model.rb +85 -13
  25. data/lib/openc3/models/trigger_model.rb +1 -1
  26. data/lib/openc3/packets/commands.rb +33 -7
  27. data/lib/openc3/packets/packet.rb +75 -71
  28. data/lib/openc3/packets/packet_config.rb +78 -29
  29. data/lib/openc3/packets/packet_item.rb +11 -103
  30. data/lib/openc3/packets/parsers/packet_item_parser.rb +177 -34
  31. data/lib/openc3/packets/parsers/xtce_converter.rb +2 -2
  32. data/lib/openc3/packets/structure.rb +29 -21
  33. data/lib/openc3/packets/structure_item.rb +31 -19
  34. data/lib/openc3/packets/telemetry.rb +37 -11
  35. data/lib/openc3/script/script.rb +1 -1
  36. data/lib/openc3/script/suite_results.rb +2 -2
  37. data/lib/openc3/subpacketizers/subpacketizer.rb +18 -0
  38. data/lib/openc3/system/system.rb +3 -3
  39. data/lib/openc3/system/target.rb +3 -32
  40. data/lib/openc3/tools/table_manager/table_config.rb +9 -1
  41. data/lib/openc3/tools/table_manager/table_item_parser.rb +2 -2
  42. data/lib/openc3/top_level.rb +45 -19
  43. data/lib/openc3/topics/decom_interface_topic.rb +31 -0
  44. data/lib/openc3/utilities/authentication.rb +25 -6
  45. data/lib/openc3/utilities/cli_generator.rb +347 -3
  46. data/lib/openc3/utilities/env_helper.rb +10 -0
  47. data/lib/openc3/utilities/logger.rb +7 -11
  48. data/lib/openc3/version.rb +6 -6
  49. data/tasks/spec.rake +2 -1
  50. data/templates/command_validator/command_validator.py +49 -0
  51. data/templates/command_validator/command_validator.rb +54 -0
  52. data/templates/tool_angular/package.json +48 -2
  53. data/templates/tool_react/package.json +51 -1
  54. data/templates/tool_svelte/package.json +48 -1
  55. data/templates/tool_vue/package.json +36 -3
  56. data/templates/widget/package.json +28 -2
  57. metadata +8 -5
  58. data/templates/tool_vue/.browserslistrc +0 -16
  59. 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
- cmd(command['value'], queue: false, scope: @scope)
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
- (@config['options'] || []).each do |option|
94
- case option[0].upcase
95
- when 'QUEUE_STATE'
96
- initial_state = option[1]
97
- else
98
- @logger.error("Unknown option passed to microservice #{@name}: #{option}")
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
- if File.exist?(File.join(gem_path, 'requirements.txt'))
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
- Logger.info "Installing python packages from requirements.txt with pypi_url=#{pypi_url}"
206
- if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
207
- pip_args = "--no-warn-script-location -i #{pypi_url} -r #{File.join(gem_path, 'requirements.txt')}"
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
- pip_args = "--no-warn-script-location -i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} -r #{File.join(gem_path, 'requirements.txt')}"
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:, username:, scope:)
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
- Store.zadd("#{scope}:#{name}", id, { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }.to_json)
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
- raise QueueError, "Queue '#{name}' is disabled. Command '#{command}' not queued."
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
- raise QueueError, "Queue '#{@name}' is disabled. Command '#{command_data['value']}' not queued."
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 = Hash.new()
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
- trigger_models = []
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
- trigger_models << model
193
- end
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, Zip::File::CREATE)
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, Zip::File::CREATE) do |zipfile|
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 = Hash.new
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
@@ -237,7 +237,7 @@ module OpenC3
237
237
 
238
238
  # ["#{@scope}__DECOM__{#{@target}}__#{@packet}"]
239
239
  def generate_topics
240
- topics = Hash.new
240
+ topics = {}
241
241
  if @left['type'] == ITEM_TYPE
242
242
  topics["#{@scope}__DECOM__{#{left['target']}}__#{left['packet']}"] = 1
243
243
  end
@@ -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
- target = System.targets[target_name]
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 packet.identify?(packet_data)
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
- if target_packets.length > 0
135
- packet = target_packets.first[1]
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
- hash = @config.cmd_id_value_hash[target_name]
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)