openc3 5.17.0 → 5.18.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.

Potentially problematic release.


This version of openc3 might be problematic. Click here for more details.

Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +1 -1
  3. data/data/config/_interfaces.yaml +4 -4
  4. data/data/config/command_modifiers.yaml +4 -0
  5. data/data/config/interface_modifiers.yaml +18 -8
  6. data/data/config/item_modifiers.yaml +34 -26
  7. data/data/config/microservice.yaml +4 -1
  8. data/data/config/param_item_modifiers.yaml +16 -0
  9. data/data/config/parameter_modifiers.yaml +29 -12
  10. data/data/config/plugins.yaml +3 -3
  11. data/data/config/screen.yaml +7 -7
  12. data/data/config/telemetry_modifiers.yaml +9 -4
  13. data/data/config/widgets.yaml +41 -14
  14. data/ext/openc3/ext/packet/packet.c +6 -0
  15. data/lib/openc3/accessors/accessor.rb +1 -0
  16. data/lib/openc3/accessors/binary_accessor.rb +170 -11
  17. data/lib/openc3/api/cmd_api.rb +39 -35
  18. data/lib/openc3/api/config_api.rb +10 -10
  19. data/lib/openc3/api/interface_api.rb +28 -21
  20. data/lib/openc3/api/limits_api.rb +29 -29
  21. data/lib/openc3/api/metrics_api.rb +3 -3
  22. data/lib/openc3/api/offline_access_api.rb +5 -5
  23. data/lib/openc3/api/router_api.rb +25 -19
  24. data/lib/openc3/api/settings_api.rb +10 -10
  25. data/lib/openc3/api/stash_api.rb +10 -10
  26. data/lib/openc3/api/target_api.rb +10 -10
  27. data/lib/openc3/api/tlm_api.rb +44 -44
  28. data/lib/openc3/conversions/bit_reverse_conversion.rb +60 -0
  29. data/lib/openc3/conversions/ip_read_conversion.rb +59 -0
  30. data/lib/openc3/conversions/ip_write_conversion.rb +61 -0
  31. data/lib/openc3/conversions/object_read_conversion.rb +88 -0
  32. data/lib/openc3/conversions/object_write_conversion.rb +38 -0
  33. data/lib/openc3/conversions.rb +6 -1
  34. data/lib/openc3/io/json_drb.rb +19 -21
  35. data/lib/openc3/io/json_rpc.rb +14 -13
  36. data/lib/openc3/microservices/microservice.rb +11 -11
  37. data/lib/openc3/microservices/scope_cleanup_microservice.rb +1 -1
  38. data/lib/openc3/microservices/timeline_microservice.rb +76 -51
  39. data/lib/openc3/models/activity_model.rb +25 -21
  40. data/lib/openc3/models/scope_model.rb +44 -13
  41. data/lib/openc3/models/sorted_model.rb +1 -1
  42. data/lib/openc3/models/target_model.rb +4 -1
  43. data/lib/openc3/operators/microservice_operator.rb +2 -2
  44. data/lib/openc3/operators/operator.rb +9 -9
  45. data/lib/openc3/packets/packet.rb +18 -1
  46. data/lib/openc3/packets/packet_config.rb +37 -16
  47. data/lib/openc3/packets/packet_item.rb +5 -0
  48. data/lib/openc3/packets/structure.rb +67 -3
  49. data/lib/openc3/packets/structure_item.rb +49 -12
  50. data/lib/openc3/script/calendar.rb +2 -2
  51. data/lib/openc3/script/extract.rb +5 -3
  52. data/lib/openc3/script/web_socket_api.rb +11 -0
  53. data/lib/openc3/topics/decom_interface_topic.rb +2 -1
  54. data/lib/openc3/topics/system_events_topic.rb +40 -0
  55. data/lib/openc3/utilities/authentication.rb +2 -1
  56. data/lib/openc3/utilities/authorization.rb +2 -2
  57. data/lib/openc3/version.rb +5 -5
  58. data/templates/tool_angular/package.json +5 -5
  59. data/templates/tool_react/package.json +8 -8
  60. data/templates/tool_svelte/package.json +10 -10
  61. data/templates/tool_vue/package.json +10 -10
  62. data/templates/widget/package.json +10 -10
  63. data/templates/widget/src/Widget.vue +0 -1
  64. metadata +22 -2
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2024, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -26,6 +26,18 @@ require 'openc3/models/plugin_model'
26
26
  require 'openc3/models/microservice_model'
27
27
  require 'openc3/models/setting_model'
28
28
  require 'openc3/models/trigger_group_model'
29
+ require 'openc3/topics/system_events_topic'
30
+
31
+ begin
32
+ require 'openc3-enterprise/models/cmd_authority_model'
33
+ rescue LoadError
34
+ # Stub out the Enterprise CmdAuthorityModel to do nothing
35
+ class CmdAuthorityModel
36
+ def self.names(scope:)
37
+ []
38
+ end
39
+ end
40
+ end
29
41
 
30
42
  module OpenC3
31
43
  class ScopeModel < Model
@@ -37,9 +49,13 @@ module OpenC3
37
49
  attr_accessor :text_log_retain_time
38
50
  attr_accessor :tool_log_retain_time
39
51
  attr_accessor :cleanup_poll_time
52
+ attr_accessor :command_authority
40
53
 
41
54
  # NOTE: The following three class methods are used by the ModelController
42
55
  # and are reimplemented to enable various Model class methods to work
56
+ #
57
+ # The scope keyword is given to support the ModelController method signature
58
+ # even though it is not used
43
59
  def self.get(name:, scope: nil)
44
60
  super(PRIMARY_KEY, name: name)
45
61
  end
@@ -52,13 +68,13 @@ module OpenC3
52
68
  super(PRIMARY_KEY)
53
69
  end
54
70
 
55
- def self.from_json(json, scope: nil)
71
+ def self.from_json(json)
56
72
  json = JSON.parse(json, :allow_nan => true, :create_additions => true) if String === json
57
73
  raise "json data is nil" if json.nil?
58
- self.new(**json.transform_keys(&:to_sym), scope: scope)
74
+ self.new(**json.transform_keys(&:to_sym))
59
75
  end
60
76
 
61
- def self.get_model(name:, scope: nil)
77
+ def self.get_model(name:)
62
78
  json = get(name: name)
63
79
  if json
64
80
  return from_json(json)
@@ -73,18 +89,15 @@ module OpenC3
73
89
  text_log_retain_time: nil,
74
90
  tool_log_retain_time: nil,
75
91
  cleanup_poll_time: 900,
76
- updated_at: nil,
77
- scope: nil
92
+ command_authority: false,
93
+ updated_at: nil
78
94
  )
79
95
  super(
80
96
  PRIMARY_KEY,
81
97
  name: name,
82
- text_log_cycle_time: text_log_cycle_time,
83
- text_log_cycle_size: text_log_cycle_size,
84
- text_log_retain_time: text_log_retain_time,
85
- tool_log_retain_time: tool_log_retain_time,
86
- cleanup_poll_time: cleanup_poll_time,
87
98
  updated_at: updated_at,
99
+ # This sets the @scope variable which is sort of redundant for the ScopeModel
100
+ # (since its the same as @name) but every model has a @scope
88
101
  scope: name
89
102
  )
90
103
  @text_log_cycle_time = text_log_cycle_time
@@ -92,6 +105,7 @@ module OpenC3
92
105
  @text_log_retain_time = text_log_retain_time
93
106
  @tool_log_retain_time = tool_log_retain_time
94
107
  @cleanup_poll_time = cleanup_poll_time
108
+ @command_authority = command_authority
95
109
  @children = []
96
110
  end
97
111
 
@@ -99,14 +113,26 @@ module OpenC3
99
113
  # Ensure there are no "." in the scope name - prevents gems accidently becoming scope names
100
114
  raise "Invalid scope name: #{@name}" if @name !~ /^[a-zA-Z0-9_-]+$/
101
115
  @name = @name.upcase
116
+ @scope = @name # Ensure @scope matches @name
102
117
  super(update: update, force: force, queued: queued)
118
+
119
+ # If we're updating the scope and disabling command_authority
120
+ # then we clear out all the existing values so it comes up fresh
121
+ if update and @command_authority == false
122
+ CmdAuthorityModel.names(scope: @name).each do |auth_name|
123
+ model = CmdAuthorityModel.get_model(name: auth_name, scope: @name)
124
+ model.destroy if model
125
+ end
126
+ end
127
+
128
+ SystemEventsTopic.write(:scope, as_json())
103
129
  end
104
130
 
105
131
  def destroy
106
132
  if @name != 'DEFAULT'
107
133
  # Remove all the plugins for this scope
108
134
  plugins = PluginModel.get_all_models(scope: @name)
109
- plugins.each do |plugin_name, plugin|
135
+ plugins.each do |_plugin_name, plugin|
110
136
  plugin.destroy
111
137
  end
112
138
  super()
@@ -115,7 +141,7 @@ module OpenC3
115
141
  end
116
142
  end
117
143
 
118
- def as_json(*a)
144
+ def as_json(*_a)
119
145
  { 'name' => @name,
120
146
  'updated_at' => @updated_at,
121
147
  'text_log_cycle_time' => @text_log_cycle_time,
@@ -123,6 +149,7 @@ module OpenC3
123
149
  'text_log_retain_time' => @text_log_retain_time,
124
150
  'tool_log_retain_time' => @tool_log_retain_time,
125
151
  'cleanup_poll_time' => @cleanup_poll_time,
152
+ 'command_authority' => @command_authority,
126
153
  }
127
154
  end
128
155
 
@@ -276,6 +303,10 @@ module OpenC3
276
303
  end
277
304
 
278
305
  def undeploy
306
+ # Delete UNKNOWN target
307
+ target = TargetModel.get_model(name: "UNKNOWN", scope: @scope)
308
+ target.destroy
309
+
279
310
  model = MicroserviceModel.get_model(name: "#{@scope}__SCOPEMULTI__#{@scope}", scope: @scope)
280
311
  model.destroy if model
281
312
  model = MicroserviceModel.get_model(name: "#{@scope}__SCOPECLEANUP__#{@scope}", scope: @scope)
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2023, OpenC3, Inc.
17
+ # All changes Copyright 2024, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -660,7 +660,10 @@ module OpenC3
660
660
  Store.del(item_map_key)
661
661
  @@item_map_cache[@name] = nil
662
662
 
663
- ConfigTopic.write({ kind: 'deleted', type: 'target', name: @name, plugin: @plugin }, scope: @scope)
663
+ topic = { kind: 'deleted', type: 'target', name: @name }
664
+ # The UNKNOWN target doesn't have an associated plugin
665
+ topic[:plugin] = @plugin if @plugin
666
+ ConfigTopic.write(topic, scope: @scope)
664
667
  rescue Exception => e
665
668
  Logger.error("Error undeploying target model #{@name} in scope #{@scope} due to #{e}")
666
669
  end
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2024, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -186,7 +186,7 @@ module OpenC3
186
186
  end
187
187
  end
188
188
 
189
- @removed_microservices.each do |microservice_name, microservice_config|
189
+ @removed_microservices.each do |microservice_name, _microservice_config|
190
190
  process = @processes[microservice_name]
191
191
  @processes.delete(microservice_name)
192
192
  @removed_processes[microservice_name] = process
@@ -127,7 +127,7 @@ module OpenC3
127
127
  # @process.io.inherit!
128
128
  @process.cwd = @work_dir
129
129
  # Spawned process should not be controlled by same Bundler constraints as spawning process
130
- ENV.each do |key, value|
130
+ ENV.each do |key, _value|
131
131
  if key =~ /^BUNDLER/
132
132
  @process.environment[key] = nil
133
133
  end
@@ -265,7 +265,7 @@ module OpenC3
265
265
  if @new_processes.length > 0
266
266
  # Start all the processes
267
267
  Logger.info("#{self.class} starting each new process...")
268
- @new_processes.each { |name, p| p.start }
268
+ @new_processes.each { |_name, p| p.start }
269
269
  @new_processes = {}
270
270
  end
271
271
  end
@@ -278,7 +278,7 @@ module OpenC3
278
278
  shutdown_processes(@changed_processes)
279
279
  break if @shutdown
280
280
 
281
- @changed_processes.each { |name, p| p.start }
281
+ @changed_processes.each { |_name, p| p.start }
282
282
  @changed_processes = {}
283
283
  end
284
284
  end
@@ -296,7 +296,7 @@ module OpenC3
296
296
 
297
297
  def respawn_dead
298
298
  @mutex.synchronize do
299
- @processes.each do |name, p|
299
+ @processes.each do |_name, p|
300
300
  break if @shutdown
301
301
  p.output_increment
302
302
  unless p.alive?
@@ -314,7 +314,7 @@ module OpenC3
314
314
  processes = processes.dup
315
315
 
316
316
  Logger.info("Commanding soft stops...")
317
- processes.each { |name, p| p.soft_stop }
317
+ processes.each { |_name, p| p.soft_stop }
318
318
  start_time = Time.now
319
319
  # Allow sufficient time for processes to shutdown cleanly
320
320
  while (Time.now - start_time) < PROCESS_SHUTDOWN_SECONDS
@@ -322,21 +322,21 @@ module OpenC3
322
322
  processes.each do |name, p|
323
323
  unless p.alive?
324
324
  processes_to_remove << name
325
- Logger.info("Soft stop process successful: #{p.cmd_line}", scope: p.scope)
325
+ Logger.debug("Soft stop process successful: #{p.cmd_line}", scope: p.scope)
326
326
  end
327
327
  end
328
328
  processes_to_remove.each do |name|
329
329
  processes.delete(name)
330
330
  end
331
331
  if processes.length <= 0
332
- Logger.info("Soft stop all successful")
332
+ Logger.debug("Soft stop all successful")
333
333
  break
334
334
  end
335
335
  sleep(0.1)
336
336
  end
337
337
  if processes.length > 0
338
- Logger.info("Commanding hard stops...")
339
- processes.each { |name, p| p.hard_stop }
338
+ Logger.debug("Commanding hard stops...")
339
+ processes.each { |_name, p| p.hard_stop }
340
340
  end
341
341
  end
342
342
 
@@ -104,6 +104,9 @@ module OpenC3
104
104
  # @return [Boolean] Whether to ignore overlapping items
105
105
  attr_accessor :ignore_overlap
106
106
 
107
+ # @return [Boolean] If this packet should be used for identification
108
+ attr_reader :virtual
109
+
107
110
  # Valid format types
108
111
  VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED, :WITH_UNITS]
109
112
 
@@ -144,6 +147,7 @@ module OpenC3
144
147
  @template = nil
145
148
  @packet_time = nil
146
149
  @ignore_overlap = false
150
+ @virtual = false
147
151
  end
148
152
 
149
153
  # Sets the target name this packet is associated with. Unidentified packets
@@ -233,6 +237,7 @@ module OpenC3
233
237
  # @return [Boolean] Whether or not the buffer of data is this packet
234
238
  def identify?(buffer)
235
239
  return false unless buffer
240
+ return false if @virtual
236
241
  return true unless @id_items
237
242
 
238
243
  @id_items.each do |item|
@@ -286,6 +291,14 @@ module OpenC3
286
291
  @packet_time = time
287
292
  end
288
293
 
294
+ def virtual=(v)
295
+ @virtual = v
296
+ if @virtual
297
+ @hidden = true
298
+ @disabled = true
299
+ end
300
+ end
301
+
289
302
  # Calculates a unique hashing sum that changes if the parts of the packet configuration change that could affect
290
303
  # the "shape" of the packet. This value is cached and that packet should not be changed if this method is being used
291
304
  def config_name
@@ -1058,7 +1071,9 @@ module OpenC3
1058
1071
  config << " ALLOW_SHORT\n" if @short_buffer_allowed
1059
1072
  config << " HAZARDOUS #{@hazardous_description.to_s.quote_if_necessary}\n" if @hazardous
1060
1073
  config << " DISABLE_MESSAGES\n" if @messages_disabled
1061
- if @disabled
1074
+ if @virtual
1075
+ config << " VIRTUAL\n"
1076
+ elsif @disabled
1062
1077
  config << " DISABLED\n"
1063
1078
  elsif @hidden
1064
1079
  config << " HIDDEN\n"
@@ -1122,6 +1137,7 @@ module OpenC3
1122
1137
  config['messages_disabled'] = true if @messages_disabled
1123
1138
  config['disabled'] = true if @disabled
1124
1139
  config['hidden'] = true if @hidden
1140
+ config['virtual'] = true if @virtual
1125
1141
  config['accessor'] = @accessor.class.to_s
1126
1142
  config['accessor_args'] = @accessor.args
1127
1143
  config['template'] = Base64.encode64(@template) if @template
@@ -1176,6 +1192,7 @@ module OpenC3
1176
1192
  packet.messages_disabled = hash['messages_disabled']
1177
1193
  packet.disabled = hash['disabled']
1178
1194
  packet.hidden = hash['hidden']
1195
+ packet.virtual = hash['virtual']
1179
1196
  if hash['accessor']
1180
1197
  begin
1181
1198
  accessor = OpenC3::const_get(hash['accessor'])
@@ -74,13 +74,13 @@ module OpenC3
74
74
  attr_reader :latest_data
75
75
 
76
76
  # @return [Hash<String>=>Hash<Array>=>Packet] Hash keyed by target name
77
- # that returns a hash keyed by an array of id values. The id values resolve to the packet
78
- # defined by that identification. Command version
77
+ # that returns a hash keyed by an array of id values. The id values resolve to the packet
78
+ # defined by that identification. Command version
79
79
  attr_reader :cmd_id_value_hash
80
80
 
81
81
  # @return [Hash<String>=>Hash<Array>=>Packet] Hash keyed by target name
82
- # that returns a hash keyed by an array of id values. The id values resolve to the packet
83
- # defined by that identification. Telemetry version
82
+ # that returns a hash keyed by an array of id values. The id values resolve to the packet
83
+ # defined by that identification. Telemetry version
84
84
  attr_reader :tlm_id_value_hash
85
85
 
86
86
  # @return [String] Language of current target (ruby or python)
@@ -219,7 +219,7 @@ module OpenC3
219
219
  'PARAMETER', 'ID_ITEM', 'ID_PARAMETER', 'ARRAY_ITEM', 'ARRAY_PARAMETER', 'APPEND_ITEM',\
220
220
  'APPEND_PARAMETER', 'APPEND_ID_ITEM', 'APPEND_ID_PARAMETER', 'APPEND_ARRAY_ITEM',\
221
221
  'APPEND_ARRAY_PARAMETER', 'ALLOW_SHORT', 'HAZARDOUS', 'PROCESSOR', 'META',\
222
- 'DISABLE_MESSAGES', 'HIDDEN', 'DISABLED', 'ACCESSOR', 'TEMPLATE', 'TEMPLATE_FILE',\
222
+ 'DISABLE_MESSAGES', 'HIDDEN', 'DISABLED', 'VIRTUAL', 'ACCESSOR', 'TEMPLATE', 'TEMPLATE_FILE',\
223
223
  'RESPONSE', 'ERROR_RESPONSE', 'SCREEN', 'RELATED_ITEM', 'IGNORE_OVERLAP'
224
224
  raise parser.error("No current packet for #{keyword}") unless @current_packet
225
225
 
@@ -232,7 +232,7 @@ module OpenC3
232
232
  'POLY_WRITE_CONVERSION', 'SEG_POLY_READ_CONVERSION', 'SEG_POLY_WRITE_CONVERSION',\
233
233
  'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'REQUIRED',\
234
234
  'LIMITS', 'LIMITS_RESPONSE', 'UNITS', 'FORMAT_STRING', 'DESCRIPTION',\
235
- 'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP', 'KEY'
235
+ 'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP', 'KEY', 'VARIABLE_BIT_SIZE'
236
236
  raise parser.error("No current item for #{keyword}") unless @current_item
237
237
 
238
238
  process_current_item(parser, keyword, params)
@@ -317,16 +317,20 @@ module OpenC3
317
317
  if @current_cmd_or_tlm == COMMAND
318
318
  PacketParser.check_item_data_types(@current_packet)
319
319
  @commands[@current_packet.target_name][@current_packet.packet_name] = @current_packet
320
- hash = @cmd_id_value_hash[@current_packet.target_name]
321
- hash = {} unless hash
322
- @cmd_id_value_hash[@current_packet.target_name] = hash
323
- update_id_value_hash(@current_packet, hash)
320
+ unless @current_packet.virtual
321
+ hash = @cmd_id_value_hash[@current_packet.target_name]
322
+ hash = {} unless hash
323
+ @cmd_id_value_hash[@current_packet.target_name] = hash
324
+ update_id_value_hash(@current_packet, hash)
325
+ end
324
326
  else
325
327
  @telemetry[@current_packet.target_name][@current_packet.packet_name] = @current_packet
326
- hash = @tlm_id_value_hash[@current_packet.target_name]
327
- hash = {} unless hash
328
- @tlm_id_value_hash[@current_packet.target_name] = hash
329
- update_id_value_hash(@current_packet, hash)
328
+ unless @current_packet.virtual
329
+ hash = @tlm_id_value_hash[@current_packet.target_name]
330
+ hash = {} unless hash
331
+ @tlm_id_value_hash[@current_packet.target_name] = hash
332
+ update_id_value_hash(@current_packet, hash)
333
+ end
330
334
  end
331
335
  @current_packet = nil
332
336
  @current_item = nil
@@ -337,7 +341,7 @@ module OpenC3
337
341
  if cmd_or_tlm == :COMMAND
338
342
  @commands[packet.target_name][packet.packet_name] = packet
339
343
 
340
- if affect_ids
344
+ if affect_ids and not packet.virtual
341
345
  hash = @cmd_id_value_hash[packet.target_name]
342
346
  hash = {} unless hash
343
347
  @cmd_id_value_hash[packet.target_name] = hash
@@ -354,7 +358,7 @@ module OpenC3
354
358
  latest_data_packets << packet unless latest_data_packets.include?(packet)
355
359
  end
356
360
 
357
- if affect_ids
361
+ if affect_ids and not packet.virtual
358
362
  hash = @tlm_id_value_hash[packet.target_name]
359
363
  hash = {} unless hash
360
364
  @tlm_id_value_hash[packet.target_name] = hash
@@ -469,6 +473,13 @@ module OpenC3
469
473
  @current_packet.hidden = true
470
474
  @current_packet.disabled = true
471
475
 
476
+ when 'VIRTUAL'
477
+ usage = "#{keyword}"
478
+ parser.verify_num_parameters(0, 0, usage)
479
+ @current_packet.hidden = true
480
+ @current_packet.disabled = true
481
+ @current_packet.virtual = true
482
+
472
483
  when 'ACCESSOR'
473
484
  usage = "#{keyword} <Accessor class name>"
474
485
  parser.verify_num_parameters(1, nil, usage)
@@ -720,6 +731,16 @@ module OpenC3
720
731
  when 'KEY'
721
732
  parser.verify_num_parameters(1, 1, 'KEY <key or path into data>')
722
733
  @current_item.key = params[0]
734
+
735
+ when 'VARIABLE_BIT_SIZE'
736
+ parser.verify_num_parameters(1, 3, 'VARIABLE_BIT_SIZE <length_item_name> <length_bits_per_count = 8> <length_value_bit_offset = 0>')
737
+
738
+ variable_bit_size = {'length_bits_per_count' => 8, 'length_value_bit_offset' => 0}
739
+ variable_bit_size['length_item_name'] = params[0].upcase
740
+ variable_bit_size['length_bits_per_count'] = Integer(params[1]) if params[1]
741
+ variable_bit_size['length_value_bit_offset'] = Integer(params[2]) if params[2]
742
+
743
+ @current_item.variable_bit_size = variable_bit_size
723
744
  end
724
745
  end
725
746
 
@@ -401,6 +401,7 @@ module OpenC3
401
401
  config << " #{self.endianness}" if self.endianness != default_endianness && self.data_type != :STRING && self.data_type != :BLOCK
402
402
  config << "\n"
403
403
 
404
+ config << " VARIABLE_BIT_SIZE '#{self.variable_bit_size['length_item_name']}' #{self.variable_bit_size['length_bits_per_count']} #{self.variable_bit_size['length_value_bit_offset']}\n" if self.variable_bit_size
404
405
  config << " REQUIRED\n" if self.required
405
406
  config << " FORMAT_STRING #{self.format_string.to_s.quote_if_necessary}\n" if self.format_string
406
407
  config << " UNITS #{self.units_full.to_s.quote_if_necessary} #{self.units.to_s.quote_if_necessary}\n" if self.units
@@ -510,6 +511,9 @@ module OpenC3
510
511
  end
511
512
 
512
513
  config['meta'] = @meta if @meta
514
+ if @variable_bit_size
515
+ config['variable_bit_size'] = @variable_bit_size
516
+ end
513
517
  config
514
518
  end
515
519
 
@@ -571,6 +575,7 @@ module OpenC3
571
575
  item.limits.values = values if values.length > 0
572
576
  end
573
577
  item.meta = hash['meta']
578
+ item.variable_bit_size = hash['variable_bit_size']
574
579
  item
575
580
  end
576
581
 
@@ -299,7 +299,6 @@ module OpenC3
299
299
  # @param overflow (see #define_item)
300
300
  # @return (see #define_item)
301
301
  def append_item(name, bit_size, data_type, array_size = nil, endianness = @default_endianness, overflow = :ERROR)
302
- raise ArgumentError, "Can't append an item after a variably sized item" if !@fixed_size
303
302
  if data_type == :DERIVED
304
303
  return define_item(name, 0, bit_size, data_type, array_size, endianness, overflow)
305
304
  else
@@ -313,12 +312,15 @@ module OpenC3
313
312
  # @param item (see #define)
314
313
  # @return (see #define)
315
314
  def append(item)
316
- raise ArgumentError, "Can't append an item after a variably sized item" if !@fixed_size
317
-
318
315
  if item.data_type == :DERIVED
319
316
  item.bit_offset = 0
320
317
  else
318
+ # We're appending a new item so set the bit_offset
321
319
  item.bit_offset = @defined_length_bits
320
+ # Also set original_bit_offset because it's currently 0
321
+ # due to PacketItemParser::create_packet_item
322
+ # get_bit_offset() returning 0 if append
323
+ item.original_bit_offset = @defined_length_bits
322
324
  end
323
325
  return define(item)
324
326
  end
@@ -337,6 +339,20 @@ module OpenC3
337
339
  def set_item(item)
338
340
  if @items[item.name]
339
341
  @items[item.name] = item
342
+ # Need to allocate space for the variable length item if its minimum size is greater than zero
343
+ if item.variable_bit_size
344
+ minimum_data_bits = 0
345
+ if (item.data_type == :INT or item.data_type == :UINT) and not item.original_array_size
346
+ # Minimum QUIC encoded integer, see https://datatracker.ietf.org/doc/html/rfc9000#name-variable-length-integer-enc
347
+ minimum_data_bits = 6
348
+ # :STRING, :BLOCK, or array item
349
+ elsif item.variable_bit_size['length_value_bit_offset'] > 0
350
+ minimum_data_bits = item.variable_bit_size['length_value_bit_offset'] * item.variable_bit_size['length_bits_per_count']
351
+ end
352
+ if minimum_data_bits > 0 and item.bit_offset >= 0 and @defined_length_bits == item.bit_offset
353
+ @defined_length_bits += minimum_data_bits
354
+ end
355
+ end
340
356
  else
341
357
  raise ArgumentError, "Unknown item: #{item.name} - Ensure item name is uppercase"
342
358
  end
@@ -566,6 +582,51 @@ module OpenC3
566
582
  end
567
583
  end
568
584
 
585
+ def calculate_total_bit_size(item)
586
+ if item.variable_bit_size
587
+ # Bit size is determined by length field
588
+ length_value = self.read(item.variable_bit_size['length_item_name'], :CONVERTED)
589
+ if item.data_type == :INT or item.data_type == :UINT and not item.original_array_size
590
+ case length_value
591
+ when 0
592
+ return 6
593
+ when 1
594
+ return 14
595
+ when 2
596
+ return 30
597
+ else
598
+ return 62
599
+ end
600
+ else
601
+ return (length_value * item.variable_bit_size['length_bits_per_count']) + item.variable_bit_size['length_value_bit_offset']
602
+ end
603
+ elsif item.original_bit_size <= 0
604
+ # Bit size is full packet length - bits before item + negative bits saved at end
605
+ return (@buffer.length * 8) - item.bit_offset + item.original_bit_size
606
+ elsif item.original_array_size and item.original_array_size <= 0
607
+ # Bit size is full packet length - bits before item + negative bits saved at end
608
+ return (@buffer.length * 8) - item.bit_offset + item.original_array_size
609
+ else
610
+ raise "Unexpected use of calculate_total_bit_size for non-variable-sized item"
611
+ end
612
+ end
613
+
614
+ def recalculate_bit_offsets
615
+ adjustment = 0
616
+ @sorted_items.each do |item|
617
+ # Anything with a negative bit offset should be left alone
618
+ if item.original_bit_offset >= 0
619
+ item.bit_offset = item.original_bit_offset + adjustment
620
+ if item.data_type != :DERIVED and (item.variable_bit_size or item.original_bit_size <= 0 or (item.original_array_size and item.original_array_size <= 0))
621
+ new_bit_size = calculate_total_bit_size(item)
622
+ if item.original_bit_size != new_bit_size
623
+ adjustment += (new_bit_size - item.original_bit_size)
624
+ end
625
+ end
626
+ end
627
+ end
628
+ end
629
+
569
630
  def internal_buffer_equals(buffer)
570
631
  raise ArgumentError, "Buffer class is #{buffer.class} but must be String" unless String === buffer
571
632
 
@@ -573,6 +634,9 @@ module OpenC3
573
634
  if @accessor.enforce_encoding
574
635
  @buffer.force_encoding(@accessor.enforce_encoding)
575
636
  end
637
+ if not @fixed_size
638
+ recalculate_bit_offsets()
639
+ end
576
640
  if @accessor.enforce_length
577
641
  if @buffer.length != @defined_length
578
642
  if @buffer.length < @defined_length