openc3 6.9.2 → 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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/data/config/command_modifiers.yaml +79 -0
  3. data/data/config/item_modifiers.yaml +5 -0
  4. data/data/config/parameter_modifiers.yaml +5 -0
  5. data/data/config/telemetry_modifiers.yaml +79 -0
  6. data/ext/openc3/ext/packet/packet.c +9 -0
  7. data/lib/openc3/accessors/accessor.rb +27 -3
  8. data/lib/openc3/accessors/binary_accessor.rb +21 -4
  9. data/lib/openc3/accessors/template_accessor.rb +3 -2
  10. data/lib/openc3/api/cmd_api.rb +7 -3
  11. data/lib/openc3/api/tlm_api.rb +17 -7
  12. data/lib/openc3/interfaces/protocols/fixed_protocol.rb +19 -13
  13. data/lib/openc3/io/json_rpc.rb +6 -0
  14. data/lib/openc3/microservices/decom_microservice.rb +97 -17
  15. data/lib/openc3/microservices/interface_decom_common.rb +32 -0
  16. data/lib/openc3/microservices/interface_microservice.rb +3 -75
  17. data/lib/openc3/microservices/queue_microservice.rb +16 -1
  18. data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +23 -0
  19. data/lib/openc3/models/plugin_model.rb +7 -1
  20. data/lib/openc3/models/queue_model.rb +32 -5
  21. data/lib/openc3/models/reaction_model.rb +25 -9
  22. data/lib/openc3/models/target_model.rb +78 -10
  23. data/lib/openc3/packets/commands.rb +33 -7
  24. data/lib/openc3/packets/packet.rb +75 -71
  25. data/lib/openc3/packets/packet_config.rb +78 -29
  26. data/lib/openc3/packets/packet_item.rb +11 -103
  27. data/lib/openc3/packets/parsers/packet_item_parser.rb +177 -34
  28. data/lib/openc3/packets/parsers/xtce_converter.rb +2 -2
  29. data/lib/openc3/packets/structure.rb +29 -21
  30. data/lib/openc3/packets/structure_item.rb +31 -19
  31. data/lib/openc3/packets/telemetry.rb +37 -11
  32. data/lib/openc3/script/suite_results.rb +2 -2
  33. data/lib/openc3/subpacketizers/subpacketizer.rb +18 -0
  34. data/lib/openc3/system/target.rb +3 -32
  35. data/lib/openc3/tools/table_manager/table_config.rb +9 -1
  36. data/lib/openc3/tools/table_manager/table_item_parser.rb +2 -2
  37. data/lib/openc3/top_level.rb +45 -19
  38. data/lib/openc3/topics/decom_interface_topic.rb +31 -0
  39. data/lib/openc3/utilities/env_helper.rb +10 -0
  40. data/lib/openc3/utilities/logger.rb +7 -11
  41. data/lib/openc3/version.rb +6 -6
  42. data/tasks/spec.rake +2 -1
  43. data/templates/tool_angular/package.json +2 -2
  44. data/templates/tool_react/package.json +1 -1
  45. data/templates/tool_svelte/package.json +1 -1
  46. data/templates/tool_vue/package.json +3 -3
  47. data/templates/widget/package.json +2 -2
  48. metadata +4 -1
@@ -76,5 +76,37 @@ module OpenC3
76
76
  end
77
77
  Topic.write_topic(ack_topic, msg_hash)
78
78
  end
79
+
80
+ def handle_get_tlm_buffer(get_tlm_buffer_json, msg_id)
81
+ get_tlm_buffer_hash = JSON.parse(get_tlm_buffer_json, allow_nan: true, create_additions: true)
82
+ target_name = get_tlm_buffer_hash['target_name']
83
+ packet_name = get_tlm_buffer_hash['packet_name']
84
+ ack_topic = "{#{@scope}__ACKCMD}TARGET__#{target_name}"
85
+ begin
86
+ packet = System.telemetry.packet(target_name, packet_name)
87
+ msg_hash = {
88
+ id: msg_id,
89
+ result: 'SUCCESS',
90
+ time: packet.packet_time.to_nsec_from_epoch,
91
+ received_time: packet.received_time.to_nsec_from_epoch,
92
+ target_name: packet.target_name,
93
+ packet_name: packet.packet_name,
94
+ received_count: packet.received_count,
95
+ stored: packet.stored.to_s,
96
+ buffer: packet.buffer(false)
97
+ }
98
+ msg_hash[:extra] = JSON.generate(packet.extra.as_json, allow_nan: true) if packet.extra
99
+
100
+ # If there is an error due to parameter out of range, etc, we rescue it so we can
101
+ # write the ACKCMD}TARGET topic and allow the source to return
102
+ rescue => error
103
+ msg_hash = {
104
+ id: msg_id,
105
+ result: 'ERROR',
106
+ message: error.message
107
+ }
108
+ end
109
+ Topic.write_topic(ack_topic, msg_hash)
110
+ end
79
111
  end
80
112
  end
@@ -554,23 +554,7 @@ module OpenC3
554
554
  target = System.targets[target_name]
555
555
  target.interface = @interface
556
556
  end
557
- @interface.tlm_target_names.each do |target_name|
558
- # Initialize the target's packet counters based on the Topic stream
559
- # Prevents packet count resetting to 0 when interface restarts
560
- begin
561
- System.telemetry.packets(target_name).each do |packet_name, packet|
562
- topic = "#{@scope}__TELEMETRY__{#{target_name}}__#{packet_name}"
563
- msg_id, msg_hash = Topic.get_newest_message(topic)
564
- if msg_id
565
- packet.received_count = msg_hash['received_count'].to_i
566
- else
567
- packet.received_count = 0
568
- end
569
- end
570
- rescue
571
- # Handle targets without telemetry
572
- end
573
- end
557
+ TargetModel.init_tlm_packet_counts(@interface.tlm_target_names, scope: @scope)
574
558
  if @interface.connect_on_startup
575
559
  @interface.state = 'ATTEMPTING'
576
560
  else
@@ -583,9 +567,6 @@ module OpenC3
583
567
  end
584
568
 
585
569
  @queued = false
586
- @sync_packet_count_data = {}
587
- @sync_packet_count_time = nil
588
- @sync_packet_count_delay_seconds = 1.0 # Sync packet counts every second
589
570
  @interface.options.each do |option_name, option_values|
590
571
  if option_name.upcase == 'OPTIMIZE_THROUGHPUT'
591
572
  @queued = true
@@ -594,7 +575,7 @@ module OpenC3
594
575
  StoreQueued.instance.set_update_interval(update_interval)
595
576
  end
596
577
  if option_name.upcase == 'SYNC_PACKET_COUNT_DELAY_SECONDS'
597
- @sync_packet_count_delay_seconds = option_values[0].to_f
578
+ TargetModel.sync_packet_count_delay_seconds = option_values[0].to_f
598
579
  end
599
580
  end
600
581
 
@@ -777,7 +758,7 @@ module OpenC3
777
758
 
778
759
  # Write to stream
779
760
  if @interface.tlm_target_enabled[packet.target_name]
780
- sync_tlm_packet_counts(packet)
761
+ TargetModel.sync_tlm_packet_counts(packet, @interface.tlm_target_names, scope: @scope)
781
762
  TelemetryTopic.write_packet(packet, queued: @queued, scope: @scope)
782
763
  end
783
764
  end
@@ -912,59 +893,6 @@ module OpenC3
912
893
  def graceful_kill
913
894
  # Just to avoid warning
914
895
  end
915
-
916
- def sync_tlm_packet_counts(packet)
917
- if @sync_packet_count_delay_seconds <= 0 or $openc3_redis_cluster
918
- # Perfect but slow method
919
- packet.received_count = TargetModel.increment_telemetry_count(packet.target_name, packet.packet_name, 1, scope: @scope)
920
- else
921
- # Eventually consistent method
922
- # Only sync every period (default 1 second) to avoid hammering Redis
923
- # This is a trade off between speed and accuracy
924
- # The packet count is eventually consistent
925
- @sync_packet_count_data[packet.target_name] ||= {}
926
- @sync_packet_count_data[packet.target_name][packet.packet_name] ||= 0
927
- @sync_packet_count_data[packet.target_name][packet.packet_name] += 1
928
-
929
- # Ensures counters change between syncs
930
- packet.received_count += 1
931
-
932
- # Check if we need to sync the packet counts
933
- if @sync_packet_count_time.nil? or (Time.now - @sync_packet_count_time) > @sync_packet_count_delay_seconds
934
- @sync_packet_count_time = Time.now
935
-
936
- inc_count = 0
937
- # Use pipeline to make this one transaction
938
- result = Store.redis_pool.pipelined do
939
- # Increment global counters for packets received
940
- @sync_packet_count_data.each do |target_name, packet_data|
941
- packet_data.each do |packet_name, count|
942
- TargetModel.increment_telemetry_count(target_name, packet_name, count, scope: @scope)
943
- inc_count += 1
944
- end
945
- end
946
- @sync_packet_count_data = {}
947
-
948
- # Get all the packet counts with the global counters
949
- @interface.tlm_target_names.each do |target_name|
950
- TargetModel.get_all_telemetry_counts(target_name, scope: @scope)
951
- end
952
- TargetModel.get_all_telemetry_counts('UNKNOWN', scope: @scope)
953
- end
954
- @interface.tlm_target_names.each do |target_name|
955
- result[inc_count].each do |packet_name, count|
956
- update_packet = System.telemetry.packet(target_name, packet_name)
957
- update_packet.received_count = count.to_i
958
- end
959
- inc_count += 1
960
- end
961
- result[inc_count].each do |packet_name, count|
962
- update_packet = System.telemetry.packet('UNKNOWN', packet_name)
963
- update_packet.received_count = count.to_i
964
- end
965
- end
966
- end
967
- end
968
896
  end
969
897
  end
970
898
 
@@ -67,7 +67,22 @@ module OpenC3
67
67
  # OPENC3_DEFAULT_QUEUE is set because commands would be re-queued to the default queue
68
68
  # NOTE: cmd() via script rescues hazardous errors and calls prompt_for_hazardous()
69
69
  # but we've overridden it to always return true and go straight to cmd_no_hazardous_check()
70
- 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
71
86
  end
72
87
  rescue StandardError => e
73
88
  @logger.error "QueueProcessor failed to process command from queue #{@name}\n#{e.message}"
@@ -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.
@@ -87,6 +89,10 @@ module OpenC3
87
89
  tf = nil
88
90
  begin
89
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
+
90
96
  # Load gem to internal gem server
91
97
  OpenC3::GemModel.put(gem_file_path, gem_install: false, scope: scope) unless validate_only
92
98
  else
@@ -209,7 +215,7 @@ module OpenC3
209
215
  # Handle python dependencies (pyproject.toml or requirements.txt)
210
216
  pyproject_path = File.join(gem_path, 'pyproject.toml')
211
217
  requirements_path = File.join(gem_path, 'requirements.txt')
212
-
218
+
213
219
  if File.exist?(pyproject_path) || File.exist?(requirements_path)
214
220
  begin
215
221
  pypi_url = get_setting('pypi_url', scope: scope)
@@ -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
@@ -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
@@ -351,8 +352,6 @@ module OpenC3
351
352
  ignored_items: [],
352
353
  limits_groups: [],
353
354
  cmd_tlm_files: [],
354
- cmd_unique_id_mode: false,
355
- tlm_unique_id_mode: false,
356
355
  id: nil,
357
356
  updated_at: nil,
358
357
  plugin: nil,
@@ -402,8 +401,6 @@ module OpenC3
402
401
  @ignored_items = ignored_items
403
402
  @limits_groups = limits_groups
404
403
  @cmd_tlm_files = cmd_tlm_files
405
- @cmd_unique_id_mode = cmd_unique_id_mode
406
- @tlm_unique_id_mode = tlm_unique_id_mode
407
404
  @id = id
408
405
  @cmd_buffer_depth = cmd_buffer_depth
409
406
  @cmd_log_cycle_time = cmd_log_cycle_time
@@ -442,8 +439,6 @@ module OpenC3
442
439
  'ignored_items' => @ignored_items,
443
440
  'limits_groups' => @limits_groups,
444
441
  'cmd_tlm_files' => @cmd_tlm_files,
445
- 'cmd_unique_id_mode' => @cmd_unique_id_mode,
446
- 'tlm_unique_id_mode' => @tlm_unique_id_mode,
447
442
  'id' => @id,
448
443
  'updated_at' => @updated_at,
449
444
  'plugin' => @plugin,
@@ -790,8 +785,6 @@ module OpenC3
790
785
  @ignored_parameters = target.ignored_parameters
791
786
  @ignored_items = target.ignored_items
792
787
  @cmd_tlm_files = target.cmd_tlm_files
793
- @cmd_unique_id_mode = target.cmd_unique_id_mode
794
- @tlm_unique_id_mode = target.tlm_unique_id_mode
795
788
  @limits_groups = system.limits.groups.keys
796
789
  update()
797
790
  end
@@ -1365,6 +1358,81 @@ module OpenC3
1365
1358
  return counts
1366
1359
  end
1367
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
+
1368
1436
  def self.increment_command_count(target_name, packet_name, count, scope:)
1369
1437
  result = Store.hincrby("#{scope}__COMMANDCNTS__{#{target_name}}", packet_name, count)
1370
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
- 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)