From bac9e709b333b67a370df3c18723e647292259d5 Mon Sep 17 00:00:00 2001 From: Ryan Melton Date: Mon, 20 Oct 2025 14:58:56 -0600 Subject: [PATCH 01/14] changes so far for subpackets and unique_id_mode --- openc3/ext/openc3/ext/packet/packet.c | 9 ++ .../microservices/decom_microservice.rb | 50 +++++++---- openc3/lib/openc3/models/target_model.rb | 10 --- openc3/lib/openc3/packets/commands.rb | 20 ++++- openc3/lib/openc3/packets/packet.rb | 80 ++++++----------- openc3/lib/openc3/packets/packet_config.rb | 86 ++++++++++++++----- openc3/lib/openc3/packets/telemetry.rb | 28 ++++-- .../openc3/subpacketizers/subpacketizer.rb | 17 ++++ openc3/lib/openc3/system/target.rb | 35 +------- openc3/python/openc3/packets/packet.py | 53 ------------ openc3/python/test/packets/test_packet.py | 13 --- openc3/spec/packets/packet_spec.rb | 16 ---- openc3/spec/system/target_spec.rb | 40 --------- 13 files changed, 188 insertions(+), 269 deletions(-) create mode 100644 openc3/lib/openc3/subpacketizers/subpacketizer.rb diff --git a/openc3/ext/openc3/ext/packet/packet.c b/openc3/ext/openc3/ext/packet/packet.c index fa75f84eba..027e2ce4e2 100644 --- a/openc3/ext/openc3/ext/packet/packet.c +++ b/openc3/ext/openc3/ext/packet/packet.c @@ -64,6 +64,9 @@ static ID id_ivar_packet_time = 0; static ID id_ivar_ignore_overlap = 0; static ID id_ivar_virtual = 0; static ID id_ivar_restricted = 0; +static ID id_ivar_subpacket = 0; +static ID id_ivar_subpacketizer = 0; +static ID id_ivar_obfuscated_items = 0; /* Sets the target name this packet is associated with. Unidentified packets * will have target name set to nil. @@ -291,6 +294,9 @@ static VALUE packet_initialize(int argc, VALUE *argv, VALUE self) rb_ivar_set(self, id_ivar_ignore_overlap, Qfalse); rb_ivar_set(self, id_ivar_virtual, Qfalse); rb_ivar_set(self, id_ivar_restricted, Qfalse); + rb_ivar_set(self, id_ivar_subpacket, Qfalse); + rb_ivar_set(self, id_ivar_subpacketizer, Qnil); + rb_ivar_set(self, id_ivar_obfuscated_items, Qnil); return self; } @@ -335,6 +341,9 @@ void Init_packet(void) id_ivar_ignore_overlap = rb_intern("@ignore_overlap"); id_ivar_virtual = rb_intern("@virtual"); id_ivar_restricted = rb_intern("@restricted"); + id_ivar_subpacket = rb_intern("@subpacket"); + id_ivar_subpacketizer = rb_intern("@subpacketizer"); + id_ivar_obfuscated_items = rb_intern("@obfuscated_items"); cPacket = rb_define_class_under(mOpenC3, "Packet", cStructure); rb_define_method(cPacket, "initialize", packet_initialize, -1); diff --git a/openc3/lib/openc3/microservices/decom_microservice.rb b/openc3/lib/openc3/microservices/decom_microservice.rb index b71db2a223..33475b4a95 100644 --- a/openc3/lib/openc3/microservices/decom_microservice.rb +++ b/openc3/lib/openc3/microservices/decom_microservice.rb @@ -154,9 +154,12 @@ def decom_packet(_topic, msg_id, msg_hash, _redis) @metric.set(name: 'decom_topic_delta_seconds', value: delta, type: 'gauge', unit: 'seconds', help: 'Delta time between data written to stream and decom start') start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + ####################################### + # Build packet object from topic data + ####################################### target_name = msg_hash["target_name"] packet_name = msg_hash["packet_name"] - packet = System.telemetry.packet(target_name, packet_name) packet.stored = ConfigParser.handle_true_false(msg_hash["stored"]) # Note: Packet time will be recalculated as part of decom so not setting @@ -168,23 +171,36 @@ def decom_packet(_topic, msg_id, msg_hash, _redis) packet.extra = extra end packet.buffer = msg_hash["buffer"] - # Processors are user code points which must be rescued - # so the TelemetryDecomTopic can write the packet - begin - packet.process # Run processors - rescue Exception => e - @error_count += 1 - @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter') - @error = e - @logger.error e.message - end - # Process all the limits and call the limits_change_callback (as necessary) - # check_limits also can call user code in the limits response - # but that is rescued separately in the limits_change_callback - packet.check_limits(System.limits_set) - # This is what actually decommutates the packet and updates the CVT - TelemetryDecomTopic.write_packet(packet, scope: @scope) + ################################################################################ + # Break packet into subpackets (if necessary) + # Subpackets are typically channelized data + ################################################################################ + subpackets = packet.subpacketize + + subpackets.each do |subpacket| + ##################################################################################### + # Run Processors + # This must be before the full decom so that processor derived values are available + ##################################################################################### + begin + subpacket.process # Run processors + rescue Exception => e + @error_count += 1 + @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter') + @error = e + @logger.error e.message + end + + ############################################################################# + # Process all the limits and call the limits_change_callback (as necessary) + # This must be before the full decom so that limits states are available + ############################################################################# + subpacket.check_limits(System.limits_set) + + # This is what actually decommutates the packet and updates the CVT + TelemetryDecomTopic.write_packet(subpacket, scope: @scope) + end diff = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start # seconds as a float @metric.set(name: 'decom_duration_seconds', value: diff, type: 'gauge', unit: 'seconds') diff --git a/openc3/lib/openc3/models/target_model.rb b/openc3/lib/openc3/models/target_model.rb index 95611a4764..165200bb2c 100644 --- a/openc3/lib/openc3/models/target_model.rb +++ b/openc3/lib/openc3/models/target_model.rb @@ -61,8 +61,6 @@ class TargetModel < Model attr_accessor :ignored_items attr_accessor :limits_groups attr_accessor :cmd_tlm_files - attr_accessor :cmd_unique_id_mode - attr_accessor :tlm_unique_id_mode attr_accessor :id attr_accessor :cmd_buffer_depth attr_accessor :cmd_log_cycle_time @@ -347,8 +345,6 @@ def initialize( ignored_items: [], limits_groups: [], cmd_tlm_files: [], - cmd_unique_id_mode: false, - tlm_unique_id_mode: false, id: nil, updated_at: nil, plugin: nil, @@ -398,8 +394,6 @@ def initialize( @ignored_items = ignored_items @limits_groups = limits_groups @cmd_tlm_files = cmd_tlm_files - @cmd_unique_id_mode = cmd_unique_id_mode - @tlm_unique_id_mode = tlm_unique_id_mode @id = id @cmd_buffer_depth = cmd_buffer_depth @cmd_log_cycle_time = cmd_log_cycle_time @@ -438,8 +432,6 @@ def as_json(*_a) 'ignored_items' => @ignored_items, 'limits_groups' => @limits_groups, 'cmd_tlm_files' => @cmd_tlm_files, - 'cmd_unique_id_mode' => @cmd_unique_id_mode, - 'tlm_unique_id_mode' => @tlm_unique_id_mode, 'id' => @id, 'updated_at' => @updated_at, 'plugin' => @plugin, @@ -786,8 +778,6 @@ def update_target_model(system) @ignored_parameters = target.ignored_parameters @ignored_items = target.ignored_items @cmd_tlm_files = target.cmd_tlm_files - @cmd_unique_id_mode = target.cmd_unique_id_mode - @tlm_unique_id_mode = target.tlm_unique_id_mode @limits_groups = system.limits.groups.keys update() end diff --git a/openc3/lib/openc3/packets/commands.rb b/openc3/lib/openc3/packets/commands.rb index 997ee6dfba..44c66643f5 100644 --- a/openc3/lib/openc3/packets/commands.rb +++ b/openc3/lib/openc3/packets/commands.rb @@ -105,7 +105,7 @@ def params(target_name, packet_name) # # @param (see #identify_tlm!) # @return (see #identify_tlm!) - def identify(packet_data, target_names = nil) + def identify(packet_data, target_names = nil, subpackets: false) identified_packet = nil target_names = target_names() unless target_names @@ -121,9 +121,10 @@ def identify(packet_data, target_names = nil) end target = System.targets[target_name] - if target and target.cmd_unique_id_mode + if target and ((not subpackets and target.cmd_unique_id_mode) or (subpackets and target.cmd_subpacket_unique_id_mode)) # Iterate through the packets and see if any represent the buffer target_packets.each do |_packet_name, packet| + next unless packet.subpacket == subpackets if packet.identify?(packet_data) identified_packet = packet break @@ -132,9 +133,20 @@ def identify(packet_data, target_names = nil) else # Do a hash lookup to quickly identify the packet if target_packets.length > 0 - packet = target_packets.first[1] + packet = nil + target_packets.each do |_packet_name, target_packet| + next if target_packet.virtual + next unless target_packet.subpacket == subpackets + packet = target_packet + break + end + return nil unless packet key = packet.read_id_values(packet_data) - hash = @config.cmd_id_value_hash[target_name] + if subpackets + hash = @config.cmd_subpacket_id_value_hash[target_name] + else + hash = @config.cmd_id_value_hash[target_name] + end identified_packet = hash[key] identified_packet = hash['CATCHALL'.freeze] unless identified_packet end diff --git a/openc3/lib/openc3/packets/packet.rb b/openc3/lib/openc3/packets/packet.rb index 762e16c44d..0b3564c36c 100644 --- a/openc3/lib/openc3/packets/packet.rb +++ b/openc3/lib/openc3/packets/packet.rb @@ -14,7 +14,7 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2024, OpenC3, Inc. +# All changes Copyright 2025, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -113,6 +113,12 @@ class Packet < Structure # @return [Boolean] If this packet is marked as restricted use attr_accessor :restricted + # @return [Boolean] If this packet is marked as a subpacket + attr_accessor :subpacket + + # @return [Subpacketizer] Subpacketizer class (optional) + attr_accessor :subpacketizer + # Valid format types VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED, :WITH_UNITS] @@ -156,7 +162,9 @@ def initialize(target_name = nil, packet_name = nil, default_endianness = :BIG_E @virtual = false @restricted = false @validator = nil - @obfuscated_items = [] + @subpacket = false + @subpacketizer = nil + @obfuscated_items = nil end # Sets the target name this packet is associated with. Unidentified packets @@ -1087,6 +1095,9 @@ def to_config(cmd_or_tlm) else config << "COMMAND #{@target_name.to_s.quote_if_necessary} #{@packet_name.to_s.quote_if_necessary} #{@default_endianness} \"#{@description}\"\n" end + if @subpacketizer + config << " SUBPACKETIZER #{@subpacketizer.class} #{@subpacketizer.args.map { |a| a.to_s.quote_if_necessary }.join(" ")}\n" + end if @accessor.class.to_s != 'OpenC3::BinaryAccessor' config << " ACCESSOR #{@accessor.class} #{@accessor.args.map { |a| a.to_s.quote_if_necessary }.join(" ")}\n" end @@ -1107,6 +1118,9 @@ def to_config(cmd_or_tlm) elsif @hidden config << " HIDDEN\n" end + if @subpacket + config << " SUBPACKET\n" + end if @restricted config << " RESTRICTED\n" end @@ -1218,60 +1232,6 @@ def as_json(*a) config end - def self.from_json(hash) - endianness = hash['endianness'] ? hash['endianness'].intern : nil # Convert to symbol - packet = Packet.new(hash['target_name'], hash['packet_name'], endianness, hash['description']) - packet.short_buffer_allowed = hash['short_buffer_allowed'] - packet.hazardous = hash['hazardous'] - packet.hazardous_description = hash['hazardous_description'] - packet.messages_disabled = hash['messages_disabled'] - packet.disabled = hash['disabled'] - packet.hidden = hash['hidden'] - packet.virtual = hash['virtual'] - packet.restricted = hash['restricted'] - if hash['accessor'] - begin - accessor = OpenC3::const_get(hash['accessor']) - if hash['accessor_args'] and hash['accessor_args'].length > 0 - packet.accessor = accessor.new(packet, *hash['accessor_args']) - else - packet.accessor = accessor.new(packet) - end - rescue => e - Logger.instance.error "#{packet.target_name} #{packet.packet_name} accessor of #{hash['accessor']} could not be found due to #{e}" - end - end - if hash['validator'] - begin - validator = OpenC3::const_get(hash['validator']) - packet.validator = validator.new(packet) - rescue => e - Logger.instance.error "#{packet.target_name} #{packet.packet_name} validator of #{hash['validator']} could not be found due to #{e}" - end - end - packet.template = Base64.decode64(hash['template']) if hash['template'] - packet.meta = hash['meta'] - # Can't convert processors - hash['items'].each do |item| - packet.define(PacketItem.from_json(item)) - end - if hash['response'] - packet.response = hash['response'] - end - if hash['error_response'] - packet.error_response = hash['error_response'] - end - if hash['screen'] - packet.screen = hash['screen'] - end - if hash['related_items'] - packet.related_items = hash['related_items'] - end - packet.ignore_overlap = hash['ignore_overlap'] - - packet - end - def decom # Read all the RAW at once because this could be optimized by the accessor json_hash = read_items(@sorted_items) @@ -1306,6 +1266,14 @@ def process(buffer = @buffer) end end + def subpacketize + if @subpacketizer + return @subpacketizer.call(self) + else + return [self] + end + end + def obfuscate() return unless @buffer return unless @obfuscated_items diff --git a/openc3/lib/openc3/packets/packet_config.rb b/openc3/lib/openc3/packets/packet_config.rb index a376b456e3..7a63244781 100644 --- a/openc3/lib/openc3/packets/packet_config.rb +++ b/openc3/lib/openc3/packets/packet_config.rb @@ -79,11 +79,21 @@ class PacketConfig # that returns a hash keyed by an array of id values. The id values resolve to the packet # defined by that identification. Command version attr_reader :cmd_id_value_hash + attr_reader :cmd_subpacket_id_value_hash + attr_reader :cmd_id_signature + attr_reader :cmd_subpacket_id_signature + attr_reader :cmd_unique_id_mode + attr_reader :cmd_subpacket_unique_id_mode # @return [Hash=>Hash=>Packet] Hash keyed by target name # that returns a hash keyed by an array of id values. The id values resolve to the packet # defined by that identification. Telemetry version attr_reader :tlm_id_value_hash + attr_reader :tlm_subpacket_id_value_hash + attr_reader :tlm_id_signature + attr_reader :tlm_subpacket_id_signature + attr_reader :tlm_unique_id_mode + attr_reader :tlm_subpacket_unique_id_mode # @return [String] Language of current target (ruby or python) attr_reader :language @@ -102,7 +112,17 @@ def initialize @latest_data = {} @warnings = [] @cmd_id_value_hash = {} + @cmd_subpacket_id_value_hash = {} + @cmd_id_signature = {} + @cmd_subpacket_id_signature = {} + @cmd_unique_id_mode = {} + @cmd_subpacket_unique_id_mode = {} @tlm_id_value_hash = {} + @tlm_subpacket_id_value_hash = {} + @tlm_id_signature = {} + @tlm_subpacket_id_signature = {} + @tlm_unique_id_mode = {} + @tlm_subpacket_unique_id_mode = {} # Create unknown packets @commands['UNKNOWN'] = {} @@ -321,18 +341,20 @@ def finish_packet PacketParser.check_item_data_types(@current_packet) @commands[@current_packet.target_name][@current_packet.packet_name] = @current_packet unless @current_packet.virtual - hash = @cmd_id_value_hash[@current_packet.target_name] - hash = {} unless hash - @cmd_id_value_hash[@current_packet.target_name] = hash - update_id_value_hash(@current_packet, hash) + if @current_packet.subpacket + build_id_metadata(@current_packet, @cmd_subpacket_id_value_hash, @cmd_subpacket_id_signature, @cmd_subpacket_unique_id_mode) + else + build_id_metadata(@current_packet, @cmd_id_value_hash, @cmd_id_signature, @cmd_unique_id_mode) + end end else @telemetry[@current_packet.target_name][@current_packet.packet_name] = @current_packet unless @current_packet.virtual - hash = @tlm_id_value_hash[@current_packet.target_name] - hash = {} unless hash - @tlm_id_value_hash[@current_packet.target_name] = hash - update_id_value_hash(@current_packet, hash) + if @current_packet.subpacket + build_id_metadata(@current_packet, @tlm_subpacket_id_value_hash, @tlm_subpacket_id_signature, @tlm_subpacket_unique_id_mode) + else + build_id_metadata(@current_packet, @tlm_id_value_hash, @tlm_id_signature, @tlm_unique_id_mode) + end end end @current_packet = nil @@ -345,10 +367,11 @@ def dynamic_add_packet(packet, cmd_or_tlm = :TELEMETRY, affect_ids: false) @commands[packet.target_name][packet.packet_name] = packet if affect_ids and not packet.virtual - hash = @cmd_id_value_hash[packet.target_name] - hash = {} unless hash - @cmd_id_value_hash[packet.target_name] = hash - update_id_value_hash(packet, hash) + if packet.subpacket + build_id_metadata(packet, @cmd_subpacket_id_value_hash, @cmd_subpacket_id_signature, @cmd_subpacket_unique_id_mode) + else + build_id_metadata(packet, @cmd_id_value_hash, @cmd_id_signature, @cmd_unique_id_mode) + end end else @telemetry[packet.target_name][packet.packet_name] = packet @@ -362,10 +385,11 @@ def dynamic_add_packet(packet, cmd_or_tlm = :TELEMETRY, affect_ids: false) end if affect_ids and not packet.virtual - hash = @tlm_id_value_hash[packet.target_name] - hash = {} unless hash - @tlm_id_value_hash[packet.target_name] = hash - update_id_value_hash(packet, hash) + if packet.subpacket + build_id_metadata(packet, @tlm_subpacket_id_value_hash, @tlm_subpacket_id_signature, @tlm_subpacket_unique_id_mode) + else + build_id_metadata(packet, @tlm_id_value_hash, @tlm_id_signature, @tlm_unique_id_mode) + end end end end @@ -398,15 +422,32 @@ def self.from_config(config, process_target_name, language = 'ruby') protected - def update_id_value_hash(packet, hash) + def build_id_metadata(packet, id_value_hash, id_signature_hash, unique_id_mode_hash) + target_id_value_hash = id_value_hash[packet.target_name] + target_id_value_hash = {} unless target_id_value_hash + id_value_hash[packet.target_name] = target_id_value_hash + update_id_value_hash(packet, target_id_value_hash, id_signature_hash, unique_id_mode_hash) + end + + def update_id_value_hash(packet, target_id_value_hash, id_signature_hash, unique_id_mode_hash) if packet.id_items.length > 0 key = [] + id_signature = "" packet.id_items.each do |item| key << item.id_value + id_signature << "__#{item.key}__#{item.bit_offset}__#{item.bit_size}__#{item.data_type}" + end + target_id_value_hash[key] = packet + target_id_signature = id_signature_hash[packet.target_name] + if target_id_signature + if id_signature != target_id_signature + unique_id_mode_hash[packet.target_name] = true + end + else + id_signature_hash[packet.target_name] = id_signature end - hash[key] = packet else - hash['CATCHALL'.freeze] = packet + target_id_value_hash['CATCHALL'.freeze] = packet end end @@ -509,12 +550,17 @@ def process_current_packet(parser, keyword, params) @current_packet.disabled = true @current_packet.virtual = true + when 'SUBPACKET' + usage = "#{keyword}" + parser.verify_num_parameters(0, 0, usage) + @current_packet.subpacket = true + when 'RESTRICTED' usage = "#{keyword}" parser.verify_num_parameters(0, 0, usage) @current_packet.restricted = true - when 'ACCESSOR', 'VALIDATOR' + when 'ACCESSOR', 'VALIDATOR', 'SUBPACKETIZER' usage = "#{keyword} ..." parser.verify_num_parameters(1, nil, usage) begin diff --git a/openc3/lib/openc3/packets/telemetry.rb b/openc3/lib/openc3/packets/telemetry.rb index e3512fbb98..6307f6c678 100644 --- a/openc3/lib/openc3/packets/telemetry.rb +++ b/openc3/lib/openc3/packets/telemetry.rb @@ -264,8 +264,8 @@ def newest_packet(target_name, item_name) # default value of nil means to search all known targets. # @return [Packet] The identified packet with its data set to the given # packet_data buffer. Returns nil if no packet could be identified. - def identify!(packet_data, target_names = nil) - identified_packet = identify(packet_data, target_names) + def identify!(packet_data, target_names = nil, subpackets: false) + identified_packet = identify(packet_data, target_names, subpackets: subpackets) identified_packet.buffer = packet_data if identified_packet return identified_packet end @@ -277,7 +277,7 @@ def identify!(packet_data, target_names = nil) # @param target_names [Array] List of target names to limit the search. The # default value of nil means to search all known targets. # @return [Packet] The identified packet, Returns nil if no packet could be identified. - def identify(packet_data, target_names = nil) + def identify(packet_data, target_names = nil, subpackets: false) target_names = target_names() unless target_names target_names.each do |target_name| @@ -293,17 +293,29 @@ def identify(packet_data, target_names = nil) end target = System.targets[target_name] - if target and target.tlm_unique_id_mode + if target and ((not subpackets and target.tlm_unique_id_mode) or (subpackets and target.tlm_subpacket_unique_id_mode)) # Iterate through the packets and see if any represent the buffer target_packets.each do |_packet_name, packet| + next unless packet.subpacket == subpackets return packet if packet.identify?(packet_data) end else # Do a hash lookup to quickly identify the packet if target_packets.length > 0 - packet = target_packets.first[1] + packet = nil + target_packets.each do |_packet_name, target_packet| + next if target_packet.virtual + next unless target_packet.subpacket == subpackets + packet = target_packet + break + end + return nil unless packet key = packet.read_id_values(packet_data) - hash = @config.tlm_id_value_hash[target_name] + if subpackets + hash = @config.tlm_subpacket_id_value_hash[target_name] + else + hash = @config.tlm_id_value_hash[target_name] + end identified_packet = hash[key] identified_packet = hash['CATCHALL'.freeze] unless identified_packet return identified_packet if identified_packet @@ -314,9 +326,9 @@ def identify(packet_data, target_names = nil) return nil end - def identify_and_define_packet(packet, target_names = nil) + def identify_and_define_packet(packet, target_names = nil, subpackets: false) if !packet.identified? - identified_packet = identify(packet.buffer(false), target_names) + identified_packet = identify(packet.buffer(false), target_names, subpackets: subpackets) return nil unless identified_packet identified_packet = identified_packet.clone diff --git a/openc3/lib/openc3/subpacketizers/subpacketizer.rb b/openc3/lib/openc3/subpacketizers/subpacketizer.rb new file mode 100644 index 0000000000..a8ad83f5b3 --- /dev/null +++ b/openc3/lib/openc3/subpacketizers/subpacketizer.rb @@ -0,0 +1,17 @@ +class Subpacketizer + attr_reader :args + + def initialize + @args = [] + end + + # Subclass and implement this method to break packet into array of subpackets + # Subpackets should be fully identified and defined + def call(packet) + return [packet] + end + + def as_json(*a) + { 'class' => self.class.name, 'args' => @args.as_json(*a) } + end +end \ No newline at end of file diff --git a/openc3/lib/openc3/system/target.rb b/openc3/lib/openc3/system/target.rb index 60a15289f3..4a38c2124d 100644 --- a/openc3/lib/openc3/system/target.rb +++ b/openc3/lib/openc3/system/target.rb @@ -14,7 +14,7 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2024, OpenC3, Inc. +# All changes Copyright 2025, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -68,12 +68,6 @@ class Target # @return [Integer] The number of telemetry packets received from this target attr_accessor :tlm_cnt - # @return [Boolean] Indicates if all command packets identify using different fields - attr_accessor :cmd_unique_id_mode - - # @return [Boolean] Indicates if telemetry packets identify using different fields - attr_accessor :tlm_unique_id_mode - # @return [String] Id of the target configuration attr_accessor :id @@ -91,13 +85,10 @@ def initialize(target_name, path, gem_path = nil) @ignored_parameters = [] @ignored_items = [] @cmd_tlm_files = [] - # @auto_screen_substitute = false @interface = nil @routers = [] @cmd_cnt = 0 @tlm_cnt = 0 - @cmd_unique_id_mode = false - @tlm_unique_id_mode = false @name = target_name.clone.upcase.freeze get_target_dir(path, gem_path) process_target_config_file() @@ -174,20 +165,8 @@ def process_file(filename) @cmd_tlm_files << filename - # when 'AUTO_SCREEN_SUBSTITUTE' - # usage = "#{keyword}" - # parser.verify_num_parameters(0, 0, usage) - # @auto_screen_substitute = true - - when 'CMD_UNIQUE_ID_MODE' - usage = "#{keyword}" - parser.verify_num_parameters(0, 0, usage) - @cmd_unique_id_mode = true - - when 'TLM_UNIQUE_ID_MODE' - usage = "#{keyword}" - parser.verify_num_parameters(0, 0, usage) - @tlm_unique_id_mode = true + when 'CMD_UNIQUE_ID_MODE', 'TLM_UNIQUE_ID_MODE' + # Deprecated - Now autodetected else # blank lines will have a nil keyword and should not raise an exception @@ -202,15 +181,7 @@ def as_json(*_a) config['requires'] = @requires config['ignored_parameters'] = @ignored_parameters config['ignored_items'] = @ignored_items - # config['auto_screen_substitute'] = true if @auto_screen_substitute config['cmd_tlm_files'] = @cmd_tlm_files - # config['filename'] = @filename - # config['interface'] = @interface.name if @interface - # config['dir'] = @dir - # config['cmd_cnt'] = @cmd_cnt - # config['tlm_cnt'] = @tlm_cnt - config['cmd_unique_id_mode'] = true if @cmd_unique_id_mode - config['tlm_unique_id_mode'] = true if @tlm_unique_id_mode config['id'] = @id config end diff --git a/openc3/python/openc3/packets/packet.py b/openc3/python/openc3/packets/packet.py index af13ce0295..99b55f9030 100644 --- a/openc3/python/openc3/packets/packet.py +++ b/openc3/python/openc3/packets/packet.py @@ -1133,59 +1133,6 @@ def as_json(self): return config - @classmethod - def from_json(cls, hash): - endianness = hash.get("endianness") - packet = Packet(hash["target_name"], hash["packet_name"], endianness, hash["description"]) - packet.short_buffer_allowed = hash.get("short_buffer_allowed") - packet.hazardous = hash.get("hazardous") - packet.hazardous_description = hash.get("hazardous_description") - packet.messages_disabled = hash.get("messages_disabled") - packet.disabled = hash.get("disabled") - packet.hidden = hash.get("hidden") - packet.virtual = hash.get("virtual") - packet.restricted = hash.get("restricted") - if "accessor" in hash: - try: - filename = class_name_to_filename(hash["accessor"]) - accessor = get_class_from_module(f"openc3.accessors.{filename}", hash["accessor"]) - if hash.get("accessor_args") and len(hash["accessor_args"]) > 0: - packet.accessor = accessor(packet, *hash["accessor_args"]) - else: - packet.accessor = accessor(packet) - except RuntimeError as error: - Logger.error( - f"{packet.target_name} {packet.packet_name} accessor of {hash['accessor']} could not be found due to {repr(error)}" - ) - if "validator" in hash: - try: - filename = class_name_to_filename(hash["validator"]) - validator = get_class_from_module(filename, hash["validator"]) - packet.validator = validator(packet) - except RuntimeError as error: - Logger.error( - f"{packet.target_name} {packet.packet_name} validator of {hash['validator']} could not be found due to {repr(error)}" - ) - if "template" in hash: - packet.template = base64.b64decode(hash["template"]) - packet.meta = hash.get("meta") - # Can't convert processors - for item in hash["items"]: - packet.define(PacketItem.from_json(item)) - - if "response" in hash: - packet.response = hash["response"] - if "error_response" in hash: - packet.error_response = hash["error_response"] - if "screen" in hash: - packet.screen = hash["screen"] - if "related_items" in hash: - packet.related_items = hash["related_items"] - if "ignore_overlap" in hash: - packet.ignore_overlap = hash["ignore_overlap"] - - return packet - def decom(self): # Read all the RAW at once because this could be optimized by the accessor json_hash = self.read_items(self.sorted_items) diff --git a/openc3/python/test/packets/test_packet.py b/openc3/python/test/packets/test_packet.py index 7b755fdf93..ec813a45f5 100644 --- a/openc3/python/test/packets/test_packet.py +++ b/openc3/python/test/packets/test_packet.py @@ -1720,19 +1720,6 @@ def test_creates_a_hash(self): self.assertIn("BinaryAccessor", json["accessor"]) # self.assertEqual(json['template'], Base64.encode64("\x00\x01\x02\x03")) - def test_creates_a_packet_from_a_hash(self): - p = Packet("tgt", "pkt") - p.template = b"\x00\x01\x02\x03" - p.append_item("test1", 8, "UINT") - p.accessor = BinaryAccessor() - packet = Packet.from_json(p.as_json()) - self.assertEqual(packet.target_name, p.target_name) - self.assertEqual(packet.packet_name, p.packet_name) - self.assertEqual(packet.accessor.__class__.__name__, "BinaryAccessor") - item = packet.sorted_items[0] - self.assertEqual(item.name, "TEST1") - self.assertEqual(packet.template, b"\x00\x01\x02\x03") - class PacketDecom(unittest.TestCase): def test_creates_decommutated_array_data(self): diff --git a/openc3/spec/packets/packet_spec.rb b/openc3/spec/packets/packet_spec.rb index 3f3850467c..80139f36e5 100644 --- a/openc3/spec/packets/packet_spec.rb +++ b/openc3/spec/packets/packet_spec.rb @@ -1658,22 +1658,6 @@ module OpenC3 end end - describe "self.from_json" do - it "creates a Packet from a hash" do - p = Packet.new("tgt", "pkt") - p.template = "\x00\x01\x02\x03" - p.append_item("test1", 8, :UINT) - p.accessor = OpenC3::XmlAccessor.new(p) - packet = Packet.from_json(p.as_json()) - expect(packet.target_name).to eql p.target_name - expect(packet.packet_name).to eql p.packet_name - expect(packet.accessor.class).to eql OpenC3::XmlAccessor - item = packet.sorted_items[0] - expect(item.name).to eql "TEST1" - expect(packet.template).to eql "\x00\x01\x02\x03" - end - end - describe "decom" do it "creates decommutated array data" do p = Packet.new("tgt", "pkt") diff --git a/openc3/spec/system/target_spec.rb b/openc3/spec/system/target_spec.rb index 8b8d4845fc..646591d989 100644 --- a/openc3/spec/system/target_spec.rb +++ b/openc3/spec/system/target_spec.rb @@ -168,46 +168,6 @@ module OpenC3 end end - context "with TLM_UNIQUE_ID_MODE" do - it "takes no parameters" do - tf = Tempfile.new('unittest') - tf.puts("") - tf.close - tgt = Target.new("TGT", 'path') - tgt.process_file(tf.path) - expect(tgt.tlm_unique_id_mode).to eql false - tf.unlink - - tf = Tempfile.new('unittest') - tf.puts("TLM_UNIQUE_ID_MODE") - tf.close - tgt = Target.new("TGT", 'path') - tgt.process_file(tf.path) - expect(tgt.tlm_unique_id_mode).to eql true - tf.unlink - end - end - - context "with CMD_UNIQUE_ID_MODE" do - it "takes no parameters" do - tf = Tempfile.new('unittest') - tf.puts("") - tf.close - tgt = Target.new("TGT", 'path') - tgt.process_file(tf.path) - expect(tgt.cmd_unique_id_mode).to eql false - tf.unlink - - tf = Tempfile.new('unittest') - tf.puts("CMD_UNIQUE_ID_MODE") - tf.close - tgt = Target.new("TGT", 'path') - tgt.process_file(tf.path) - expect(tgt.cmd_unique_id_mode).to eql true - tf.unlink - end - end - context "with COMMANDS and TELEMETRY" do it "takes 1 parameters" do tf = Tempfile.new('unittest') From ac59a7ec607c0e4bf52f829de41e37d46867fdf3 Mon Sep 17 00:00:00 2001 From: Ryan Melton Date: Wed, 22 Oct 2025 16:12:27 -0600 Subject: [PATCH 02/14] ruby existing tests passing --- openc3/data/config/command_modifiers.yaml | 17 +++++++++ openc3/data/config/telemetry_modifiers.yaml | 17 +++++++++ .../interfaces/protocols/fixed_protocol.rb | 32 +++++++++------- openc3/lib/openc3/packets/commands.rb | 38 +++++++++++++------ openc3/lib/openc3/packets/telemetry.rb | 38 +++++++++++++------ .../DefaultWidget/DefaultWidget.umd.min.js | 0 .../DefaultWidget.umd.min.js.map | 0 .../interfaces/mqtt_stream_interface_spec.rb | 32 ++++++++-------- .../protocols/fixed_protocol_spec.rb | 21 +++++----- .../protocols/ignore_packet_protocol_spec.rb | 9 +++-- openc3/spec/packets/commands_spec.rb | 7 ++-- openc3/spec/packets/telemetry_spec.rb | 7 ++-- 12 files changed, 142 insertions(+), 76 deletions(-) create mode 100644 openc3/spec/install/tools/widgets/DefaultWidget/DefaultWidget.umd.min.js create mode 100644 openc3/spec/install/tools/widgets/DefaultWidget/DefaultWidget.umd.min.js.map diff --git a/openc3/data/config/command_modifiers.yaml b/openc3/data/config/command_modifiers.yaml index 24a8a52e64..3449b99677 100644 --- a/openc3/data/config/command_modifiers.yaml +++ b/openc3/data/config/command_modifiers.yaml @@ -176,6 +176,19 @@ ACCESSOR: description: Additional argument passed to the accessor class constructor values: .+ since: 5.0.10 +SUBPACKETIZER: + summary: Defines a class used to break up the packet into subpackets before decom + description: Defines a class used to break up the packet into subpackets before decom. Defaults to nil/None. + parameters: + - name: Subpacketizer Class Name + required: true + description: The name of the Subpacketizer class + values: .+ + - name: Argument + required: false + description: Additional argument passed to the Subpacketizer class constructor + values: .+ + since: 6.10.0 TEMPLATE: summary: Defines a template string used to initialize the command before default values are filled in description: Generally the template string is formatted in JSON or HTML and then values are filled in with @@ -256,6 +269,10 @@ RESTRICTED: summary: Marks this packet as restricted and will require approval if critical commanding is enabled description: Used as one of the two types of critical commands (HAZARDOUS and RESTRICTED) since: 5.20.0 +SUBPACKET: + summary: Marks this packet as as a subpacket which will exclude it from Interface level identification + description: Used with a SUBPACKETIZER to breakup up packets into subpackets at decom time + since: 6.10.0 VALIDATOR: summary: Defines a validator class for a command description: Validator class is used to validate the command success or failure with both a pre_check and post_check method. diff --git a/openc3/data/config/telemetry_modifiers.yaml b/openc3/data/config/telemetry_modifiers.yaml index 44e877385d..7d4bbcb01c 100644 --- a/openc3/data/config/telemetry_modifiers.yaml +++ b/openc3/data/config/telemetry_modifiers.yaml @@ -173,6 +173,19 @@ ACCESSOR: description: The name of the accessor class values: .+ since: 5.0.10 +SUBPACKETIZER: + summary: Defines a class used to break up the packet into subpackets before decom + description: Defines a class used to break up the packet into subpackets before decom. Defaults to nil/None. + parameters: + - name: Subpacketizer Class Name + required: true + description: The name of the Subpacketizer class + values: .+ + - name: Argument + required: false + description: Additional argument passed to the Subpacketizer class constructor + values: .+ + since: 6.10.0 TEMPLATE: summary: Defines a template string used to pull telemetry values from a string buffer parameters: @@ -198,3 +211,7 @@ VIRTUAL: summary: Marks this packet as virtual and not participating in identification description: Used for packet definitions that can be used as structures for items with a given packet. since: 5.18.0 +SUBPACKET: + summary: Marks this packet as as a subpacket which will exclude it from Interface level identification + description: Used with a SUBPACKETIZER to breakup up packets into subpackets at decom time + since: 6.10.0 \ No newline at end of file diff --git a/openc3/lib/openc3/interfaces/protocols/fixed_protocol.rb b/openc3/lib/openc3/interfaces/protocols/fixed_protocol.rb index 69846b9505..edc26422df 100644 --- a/openc3/lib/openc3/interfaces/protocols/fixed_protocol.rb +++ b/openc3/lib/openc3/interfaces/protocols/fixed_protocol.rb @@ -89,12 +89,10 @@ def identify_and_finish_packet begin if @telemetry target_packets = System.telemetry.packets(target_name) - target = System.targets[target_name] - unique_id_mode = target.tlm_unique_id_mode if target + unique_id_mode = System.telemetry.tlm_unique_id_mode(target_name) else target_packets = System.commands.packets(target_name) - target = System.targets[target_name] - unique_id_mode = target.cmd_unique_id_mode if target + unique_id_mode = System.commands.cmd_unique_id_mode(target_name) end rescue RuntimeError # No commands/telemetry for this target @@ -103,7 +101,7 @@ def identify_and_finish_packet if unique_id_mode target_packets.each do |_packet_name, packet| - if packet.identify?(@data[@discard_leading_bytes..-1]) + if not packet.subpacket and packet.identify?(@data[@discard_leading_bytes..-1]) # identify? handles virtual identified_packet = packet break end @@ -111,15 +109,23 @@ def identify_and_finish_packet else # Do a hash lookup to quickly identify the packet if target_packets.length > 0 - packet = target_packets.first[1] - key = packet.read_id_values(@data[@discard_leading_bytes..-1]) - if @telemetry - hash = System.telemetry.config.tlm_id_value_hash[target_name] - else - hash = System.commands.config.cmd_id_value_hash[target_name] + packet = nil + target_packets.each do |_packet_name, target_packet| + next if target_packet.virtual + next if target_packet.subpacket + packet = target_packet + break + end + if packet + key = packet.read_id_values(@data[@discard_leading_bytes..-1]) + if @telemetry + hash = System.telemetry.config.tlm_id_value_hash[target_name] + else + hash = System.commands.config.cmd_id_value_hash[target_name] + end + identified_packet = hash[key] + identified_packet = hash['CATCHALL'.freeze] unless identified_packet end - identified_packet = hash[key] - identified_packet = hash['CATCHALL'.freeze] unless identified_packet end end diff --git a/openc3/lib/openc3/packets/commands.rb b/openc3/lib/openc3/packets/commands.rb index 44c66643f5..14a484114c 100644 --- a/openc3/lib/openc3/packets/commands.rb +++ b/openc3/lib/openc3/packets/commands.rb @@ -120,27 +120,33 @@ def identify(packet_data, target_names = nil, subpackets: false) next end - target = System.targets[target_name] - if target and ((not subpackets and target.cmd_unique_id_mode) or (subpackets and target.cmd_subpacket_unique_id_mode)) + if (not subpackets and System.commands.cmd_unique_id_mode(target_name)) or (subpackets and System.commands.cmd_subpacket_unique_id_mode(target_name)) # Iterate through the packets and see if any represent the buffer target_packets.each do |_packet_name, packet| - next unless packet.subpacket == subpackets - if packet.identify?(packet_data) + if subpackets + next unless packet.subpacket + else + next if packet.subpacket + end + if packet.identify?(packet_data) # Handles virtual identified_packet = packet break end end else # Do a hash lookup to quickly identify the packet - if target_packets.length > 0 - packet = nil - target_packets.each do |_packet_name, target_packet| - next if target_packet.virtual - next unless target_packet.subpacket == subpackets - packet = target_packet - break + packet = nil + target_packets.each do |_packet_name, target_packet| + next if target_packet.virtual + if subpackets + next unless target_packet.subpacket + else + next if target_packet.subpacket end - return nil unless packet + packet = target_packet + break + end + if packet key = packet.read_id_values(packet_data) if subpackets hash = @config.cmd_subpacket_id_value_hash[target_name] @@ -276,6 +282,14 @@ def dynamic_add_packet(packet, affect_ids: false) @config.dynamic_add_packet(packet, :COMMAND, affect_ids: affect_ids) end + def cmd_unique_id_mode(target_name) + return @config.cmd_unique_id_mode[target_name.upcase] + end + + def cmd_subpacket_unique_id_mode(target_name) + return @config.cmd_subpacket_unique_id_mode[target_name.upcase] + end + protected def set_parameters(command, params, range_checking) diff --git a/openc3/lib/openc3/packets/telemetry.rb b/openc3/lib/openc3/packets/telemetry.rb index 6307f6c678..f8967d00c0 100644 --- a/openc3/lib/openc3/packets/telemetry.rb +++ b/openc3/lib/openc3/packets/telemetry.rb @@ -292,24 +292,30 @@ def identify(packet_data, target_names = nil, subpackets: false) next end - target = System.targets[target_name] - if target and ((not subpackets and target.tlm_unique_id_mode) or (subpackets and target.tlm_subpacket_unique_id_mode)) + if (not subpackets and System.telemetry.tlm_unique_id_mode(target_name)) or (subpackets and System.telemetry.tlm_subpacket_unique_id_mode(target_name)) # Iterate through the packets and see if any represent the buffer target_packets.each do |_packet_name, packet| - next unless packet.subpacket == subpackets - return packet if packet.identify?(packet_data) + if subpackets + next unless packet.subpacket + else + next if packet.subpacket + end + return packet if packet.identify?(packet_data) # Handles virtual end else # Do a hash lookup to quickly identify the packet - if target_packets.length > 0 - packet = nil - target_packets.each do |_packet_name, target_packet| - next if target_packet.virtual - next unless target_packet.subpacket == subpackets - packet = target_packet - break + packet = nil + target_packets.each do |_packet_name, target_packet| + next if target_packet.virtual + if subpackets + next unless target_packet.subpacket + else + next if target_packet.subpacket end - return nil unless packet + packet = target_packet + break + end + if packet key = packet.read_id_values(packet_data) if subpackets hash = @config.tlm_subpacket_id_value_hash[target_name] @@ -437,5 +443,13 @@ def all def dynamic_add_packet(packet, affect_ids: false) @config.dynamic_add_packet(packet, :TELEMETRY, affect_ids: affect_ids) end + + def tlm_unique_id_mode(target_name) + return @config.tlm_unique_id_mode[target_name.upcase] + end + + def tlm_subpacket_unique_id_mode(target_name) + return @config.tlm_subpacket_unique_id_mode[target_name.upcase] + end end # class Telemetry end diff --git a/openc3/spec/install/tools/widgets/DefaultWidget/DefaultWidget.umd.min.js b/openc3/spec/install/tools/widgets/DefaultWidget/DefaultWidget.umd.min.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openc3/spec/install/tools/widgets/DefaultWidget/DefaultWidget.umd.min.js.map b/openc3/spec/install/tools/widgets/DefaultWidget/DefaultWidget.umd.min.js.map new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openc3/spec/interfaces/mqtt_stream_interface_spec.rb b/openc3/spec/interfaces/mqtt_stream_interface_spec.rb index f09077c293..c886a15411 100644 --- a/openc3/spec/interfaces/mqtt_stream_interface_spec.rb +++ b/openc3/spec/interfaces/mqtt_stream_interface_spec.rb @@ -21,7 +21,7 @@ module OpenC3 describe MqttStreamInterface do - MQTT_CLIENT = 'MQTT::Client'.freeze + MY_MQTT_CLIENT = 'MQTT::Client'.freeze before(:all) do setup_system() @@ -49,7 +49,7 @@ module OpenC3 describe "connect" do it "sets various ssl settings based on options" do - double = double(MQTT_CLIENT) + double = double(MY_MQTT_CLIENT) expect(double).to receive(:ack_timeout=).with(10.0) expect(double).to receive(:host=).with('localhost') expect(double).to receive(:port=).with(1883) @@ -78,7 +78,7 @@ module OpenC3 end it "sets ssl even without cert_file, key_file, or ca_file" do - double = double(MQTT_CLIENT).as_null_object + double = double(MY_MQTT_CLIENT).as_null_object expect(double).to receive(:ssl=).with(true) expect(double).to receive(:connected?).and_return(true) allow(MQTT::Client).to receive(:new).and_return(double) @@ -91,7 +91,7 @@ module OpenC3 describe "disconnect" do it "disconnects the mqtt client" do - double = double(MQTT_CLIENT).as_null_object + double = double(MY_MQTT_CLIENT).as_null_object expect(double).to receive(:connect) expect(double).to receive(:disconnect) allow(MQTT::Client).to receive(:new).and_return(double) @@ -106,7 +106,7 @@ module OpenC3 describe "read" do it "reads a message from the mqtt client" do - double = double(MQTT_CLIENT).as_null_object + double = double(MY_MQTT_CLIENT).as_null_object expect(double).to receive(:connect) expect(double).to receive(:connected?).and_return(true) expect(double).to receive(:get).and_return(['HEALTH_STATUS', "\x00\x01\x02\x03\x04\x05"]) @@ -126,7 +126,7 @@ module OpenC3 end it "disconnects if the mqtt client returns no data" do - double = double(MQTT_CLIENT).as_null_object + double = double(MY_MQTT_CLIENT).as_null_object expect(double).to receive(:connect) expect(double).to receive(:connected?).and_return(true) expect(double).to receive(:get).and_return(['HEALTH_STATUS', nil]) @@ -144,7 +144,7 @@ module OpenC3 describe "write" do it "writes a message to the mqtt client" do - double = double(MQTT_CLIENT).as_null_object + double = double(MY_MQTT_CLIENT).as_null_object expect(double).to receive(:connect) expect(double).to receive(:connected?).and_return(true) allow(MQTT::Client).to receive(:new).and_return(double) @@ -161,16 +161,16 @@ module OpenC3 describe "details" do it "returns detailed interface information" do i = MqttStreamInterface.new('mqtt-server', '8883', true, 'cmd_topic', 'tlm_topic') - + details = i.details - + expect(details).to be_a(Hash) expect(details['hostname']).to eql('mqtt-server') expect(details['port']).to eql(8883) expect(details['ssl']).to be true expect(details['write_topic']).to eql('cmd_topic') expect(details['read_topic']).to eql('tlm_topic') - + # Check that base interface details are included expect(details['name']).to eql('MqttStreamInterface') expect(details).to have_key('read_allowed') @@ -186,16 +186,16 @@ module OpenC3 i.set_option('KEY', ['key_content']) i.set_option('CA_FILE', ['ca_content']) i.set_option('ACK_TIMEOUT', ['15.0']) - + details = i.details - + expect(details['username']).to eql('test_user') expect(details['password']).to eql('Set') expect(details['cert']).to eql('Set') expect(details['key']).to eql('Set') expect(details['ca_file']).to eql('Set') expect(details['ack_timeout']).to eql(15.0) - + # Verify sensitive options are removed from options hash expect(details['options']).to_not have_key('PASSWORD') expect(details['options']).to_not have_key('CERT') @@ -205,15 +205,15 @@ module OpenC3 it "handles missing sensitive fields" do i = MqttStreamInterface.new('mqtt-server', '1883', false, 'cmd_topic', 'tlm_topic') - + details = i.details - + expect(details['hostname']).to eql('mqtt-server') expect(details['port']).to eql(1883) expect(details['ssl']).to be false expect(details['write_topic']).to eql('cmd_topic') expect(details['read_topic']).to eql('tlm_topic') - + expect(details).to_not have_key('password') expect(details).to_not have_key('cert') expect(details).to_not have_key('key') diff --git a/openc3/spec/interfaces/protocols/fixed_protocol_spec.rb b/openc3/spec/interfaces/protocols/fixed_protocol_spec.rb index 9c96521fce..98935233fb 100644 --- a/openc3/spec/interfaces/protocols/fixed_protocol_spec.rb +++ b/openc3/spec/interfaces/protocols/fixed_protocol_spec.rb @@ -23,6 +23,7 @@ require 'spec_helper' require 'openc3/interfaces/protocols/fixed_protocol' require 'openc3/interfaces/interface' +require 'openc3/interfaces/stream_interface' require 'openc3/streams/stream' module OpenC3 @@ -114,7 +115,6 @@ def read end it "reads telemetry data from the stream" do - target = System.targets['SYSTEM'] @interface.add_protocol(FixedProtocol, [1], :READ_WRITE) @interface.instance_variable_set(:@stream, FixedStream.new) @interface.target_names = ['SYSTEM'] @@ -130,7 +130,7 @@ def read expect(packet.received_time.to_f).to be_within(0.1).of(Time.now.to_f) expect(packet.target_name).to eql 'SYSTEM' expect(packet.packet_name).to eql 'LIMITS_CHANGE' - target.tlm_unique_id_mode = true + System.telemetry.config.tlm_unique_id_mode['SYSTEM'] = true $index = 1 packet = @interface.read expect(packet.received_time.to_f).to be_within(0.1).of(Time.now.to_f) @@ -141,11 +141,10 @@ def read expect(packet.received_time.to_f).to be_within(0.1).of(Time.now.to_f) expect(packet.target_name).to eql 'SYSTEM' expect(packet.packet_name).to eql 'LIMITS_CHANGE' - target.tlm_unique_id_mode = false + System.telemetry.config.tlm_unique_id_mode['SYSTEM'] = false end it "reads command data from the stream" do - target = System.targets['SYSTEM'] packet = System.commands.packet("SYSTEM", "STARTLOGGING") packet.restore_defaults $buffer = packet.buffer.clone @@ -165,19 +164,19 @@ def read @interface.target_names = ['SYSTEM'] @interface.cmd_target_names = ['SYSTEM'] @interface.tlm_target_names = ['SYSTEM'] - target.cmd_unique_id_mode = false + System.commands.config.cmd_unique_id_mode['SYSTEM'] = false packet = @interface.read expect(packet.received_time.to_f).to be_within(0.01).of(Time.now.to_f) expect(packet.target_name).to eql 'SYSTEM' expect(packet.packet_name).to eql 'STARTLOGGING' expect(packet.buffer).to eql $buffer - target.cmd_unique_id_mode = true + System.commands.config.cmd_unique_id_mode['SYSTEM'] = true packet = @interface.read expect(packet.received_time.to_f).to be_within(0.01).of(Time.now.to_f) expect(packet.target_name).to eql 'SYSTEM' expect(packet.packet_name).to eql 'STARTLOGGING' expect(packet.buffer).to eql $buffer - target.cmd_unique_id_mode = false + System.commands.config.cmd_unique_id_mode['SYSTEM'] = false end it "breaks apart telemetry data from the stream" do @@ -227,7 +226,7 @@ def read @interface.add_protocol(FixedProtocol, [2, 1, '0xDEADBEEF', false, true], :READ_WRITE) protocol = @interface.write_protocols[0] details = protocol.write_details - + expect(details).to be_a(Hash) expect(details['name']).to eq('FixedProtocol') expect(details.key?('write_data_input_time')).to be true @@ -240,7 +239,7 @@ def read @interface.add_protocol(FixedProtocol, [4, 2, '0x1234', true, false], :READ_WRITE) protocol = @interface.write_protocols[0] details = protocol.write_details - + expect(details['min_id_size']).to eq(4) expect(details['discard_leading_bytes']).to eq(2) expect(details['sync_pattern']).to eq("\x12\x34".inspect) @@ -254,7 +253,7 @@ def read @interface.add_protocol(FixedProtocol, [1], :READ) protocol = @interface.read_protocols[0] details = protocol.read_details - + expect(details).to be_a(Hash) expect(details['name']).to eq('FixedProtocol') expect(details.key?('read_data_input_time')).to be true @@ -267,7 +266,7 @@ def read @interface.add_protocol(FixedProtocol, [8, 6, '0x1ACFFC1D', false, true, false], :READ_WRITE) protocol = @interface.read_protocols[0] details = protocol.read_details - + expect(details['min_id_size']).to eq(8) expect(details['discard_leading_bytes']).to eq(6) expect(details['sync_pattern']).to eq("\x1A\xCF\xFC\x1D".inspect) diff --git a/openc3/spec/interfaces/protocols/ignore_packet_protocol_spec.rb b/openc3/spec/interfaces/protocols/ignore_packet_protocol_spec.rb index 3f0b03bee2..83140e94af 100644 --- a/openc3/spec/interfaces/protocols/ignore_packet_protocol_spec.rb +++ b/openc3/spec/interfaces/protocols/ignore_packet_protocol_spec.rb @@ -23,6 +23,7 @@ require 'spec_helper' require 'openc3/interfaces/protocols/ignore_packet_protocol' require 'openc3/interfaces/interface' +require 'openc3/interfaces/stream_interface' require 'openc3/streams/stream' module OpenC3 @@ -305,7 +306,7 @@ def write(data); $buffer = data; end @interface.add_protocol(IgnorePacketProtocol, ['SYSTEM', 'META'], :READ_WRITE) protocol = @interface.write_protocols[0] details = protocol.write_details - + expect(details).to be_a(Hash) expect(details['name']).to eq('IgnorePacketProtocol') expect(details.key?('write_data_input_time')).to be true @@ -318,7 +319,7 @@ def write(data); $buffer = data; end @interface.add_protocol(IgnorePacketProtocol, ['INST', 'HEALTH_STATUS'], :READ_WRITE) protocol = @interface.write_protocols[0] details = protocol.write_details - + expect(details['target_name']).to eq('INST') expect(details['packet_name']).to eq('HEALTH_STATUS') end @@ -329,7 +330,7 @@ def write(data); $buffer = data; end @interface.add_protocol(IgnorePacketProtocol, ['SYSTEM', 'META'], :READ_WRITE) protocol = @interface.read_protocols[0] details = protocol.read_details - + expect(details).to be_a(Hash) expect(details['name']).to eq('IgnorePacketProtocol') expect(details.key?('read_data_input_time')).to be true @@ -342,7 +343,7 @@ def write(data); $buffer = data; end @interface.add_protocol(IgnorePacketProtocol, ['INST', 'ADCS'], :READ_WRITE) protocol = @interface.read_protocols[0] details = protocol.read_details - + expect(details['target_name']).to eq('INST') expect(details['packet_name']).to eq('ADCS') end diff --git a/openc3/spec/packets/commands_spec.rb b/openc3/spec/packets/commands_spec.rb index a2e0f93006..86938113ad 100644 --- a/openc3/spec/packets/commands_spec.rb +++ b/openc3/spec/packets/commands_spec.rb @@ -180,8 +180,7 @@ module OpenC3 it "works in unique id mode or not" do System.targets["TGT1"] = Target.new("TGT1", Dir.pwd) - target = System.targets["TGT1"] - target.cmd_unique_id_mode = false + System.commands.config.cmd_unique_id_mode["TGT1"] = false buffer = "\x01\x02\x03\x04" pkt = @cmd.identify(buffer, ["TGT1"]) pkt.enable_method_missing @@ -189,7 +188,7 @@ module OpenC3 expect(pkt.item2).to eql 2 expect(pkt.item3).to eql 3 expect(pkt.item4).to eql 4 - target.cmd_unique_id_mode = true + System.commands.config.cmd_unique_id_mode["TGT1"] = true buffer = "\x01\x02\x01\x02" pkt = @cmd.identify(buffer, ["TGT1"]) pkt.enable_method_missing @@ -197,7 +196,7 @@ module OpenC3 expect(pkt.item2).to eql 2 expect(pkt.item3).to eql 1 expect(pkt.item4).to eql 2 - target.cmd_unique_id_mode = false + System.commands.config.cmd_unique_id_mode["TGT1"] = false end it "returns nil with unknown targets given" do diff --git a/openc3/spec/packets/telemetry_spec.rb b/openc3/spec/packets/telemetry_spec.rb index c74a56e873..79fc49253f 100644 --- a/openc3/spec/packets/telemetry_spec.rb +++ b/openc3/spec/packets/telemetry_spec.rb @@ -276,9 +276,8 @@ module OpenC3 it "works in unique id mode and not" do System.targets["TGT1"] = Target.new("TGT1", Dir.pwd) - target = System.targets["TGT1"] buffer = "\x01\x02\x03\x04" - target.tlm_unique_id_mode = false + System.telemetry.config.tlm_unique_id_mode["TGT1"] = false pkt = @tlm.identify!(buffer, ["TGT1"]) pkt.enable_method_missing expect(pkt.item1).to eql 1 @@ -286,7 +285,7 @@ module OpenC3 expect(pkt.item3).to eql 6.0 expect(pkt.item4).to eql 8.0 buffer = "\x01\x02\x01\x02" - target.tlm_unique_id_mode = true + System.telemetry.config.tlm_unique_id_mode["TGT1"] = true @tlm.identify!(buffer, ["TGT1"]) pkt = @tlm.packet("TGT1", "PKT1") pkt.enable_method_missing @@ -294,7 +293,7 @@ module OpenC3 expect(pkt.item2).to eql 2 expect(pkt.item3).to eql 2.0 expect(pkt.item4).to eql 4.0 - target.tlm_unique_id_mode = false + System.telemetry.config.tlm_unique_id_mode["TGT1"] = false end it "returns nil with unknown targets given" do From 61768c0cda4f1bc3e169507b9a5c27b47fa8f1d2 Mon Sep 17 00:00:00 2001 From: Ryan Melton Date: Wed, 22 Oct 2025 16:31:07 -0600 Subject: [PATCH 03/14] add ruby unit tests --- openc3/lib/openc3/packets/packet_config.rb | 2 +- .../protocols/fixed_protocol_spec.rb | 86 ++++++++++++++- openc3/spec/packets/commands_spec.rb | 71 ++++++++++++ openc3/spec/packets/packet_config_spec.rb | 103 ++++++++++++++++++ openc3/spec/packets/packet_spec.rb | 53 +++++++++ openc3/spec/packets/telemetry_spec.rb | 78 +++++++++++++ 6 files changed, 391 insertions(+), 2 deletions(-) diff --git a/openc3/lib/openc3/packets/packet_config.rb b/openc3/lib/openc3/packets/packet_config.rb index 7a63244781..6db2ceeabc 100644 --- a/openc3/lib/openc3/packets/packet_config.rb +++ b/openc3/lib/openc3/packets/packet_config.rb @@ -242,7 +242,7 @@ def process_file(filename, process_target_name, language = 'ruby') 'APPEND_PARAMETER', 'APPEND_ID_ITEM', 'APPEND_ID_PARAMETER', 'APPEND_ARRAY_ITEM',\ 'APPEND_ARRAY_PARAMETER', 'ALLOW_SHORT', 'HAZARDOUS', 'PROCESSOR', 'META',\ 'DISABLE_MESSAGES', 'HIDDEN', 'DISABLED', 'VIRTUAL', 'RESTRICTED', 'ACCESSOR', 'TEMPLATE', 'TEMPLATE_FILE',\ - 'RESPONSE', 'ERROR_RESPONSE', 'SCREEN', 'RELATED_ITEM', 'IGNORE_OVERLAP', 'VALIDATOR' + 'RESPONSE', 'ERROR_RESPONSE', 'SCREEN', 'RELATED_ITEM', 'IGNORE_OVERLAP', 'VALIDATOR', 'SUBPACKET', 'SUBPACKETIZER' raise parser.error("No current packet for #{keyword}") unless @current_packet process_current_packet(parser, keyword, params) diff --git a/openc3/spec/interfaces/protocols/fixed_protocol_spec.rb b/openc3/spec/interfaces/protocols/fixed_protocol_spec.rb index 98935233fb..ea59a7b429 100644 --- a/openc3/spec/interfaces/protocols/fixed_protocol_spec.rb +++ b/openc3/spec/interfaces/protocols/fixed_protocol_spec.rb @@ -25,6 +25,7 @@ require 'openc3/interfaces/interface' require 'openc3/interfaces/stream_interface' require 'openc3/streams/stream' +require 'tempfile' module OpenC3 describe FixedProtocol do @@ -161,7 +162,7 @@ def read # Require 8 bytes, discard 6 leading bytes, use 0x1ACFFC1D sync, telemetry = false (command) @interface.add_protocol(FixedProtocol, [8, 6, '0x1ACFFC1D', false], :READ_WRITE) @interface.instance_variable_set(:@stream, FixedStream2.new) - @interface.target_names = ['SYSTEM'] + @interface.target_names = ['SfYSTEM'] @interface.cmd_target_names = ['SYSTEM'] @interface.tlm_target_names = ['SYSTEM'] System.commands.config.cmd_unique_id_mode['SYSTEM'] = false @@ -274,5 +275,88 @@ def read expect(details['fill_fields']).to eq(true) end end + + describe "packet identification with subpackets" do + before(:all) do + setup_system() + end + + before(:each) do + tf = Tempfile.new('unittest') + tf.puts 'TELEMETRY TEST PKT1 BIG_ENDIAN "Normal Packet"' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 1 "Item1"' + tf.puts ' APPEND_ITEM item2 8 UINT "Item2"' + tf.puts 'TELEMETRY TEST SUB1 BIG_ENDIAN "Subpacket"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 10 "Item1"' + tf.puts ' APPEND_ITEM item2 8 UINT "Item2"' + tf.puts 'TELEMETRY TEST VIRTUAL_PKT BIG_ENDIAN "Virtual Packet"' + tf.puts ' VIRTUAL' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 99 "Item1"' + tf.puts ' APPEND_ITEM item2 8 UINT "Item2"' + tf.close + + pc = PacketConfig.new + pc.process_file(tf.path, "TEST") + telemetry = Telemetry.new(pc) + tf.unlink + allow(System).to receive_message_chain(:telemetry).and_return(telemetry) + end + + $subpacket_index = 0 + class SubpacketStream < Stream + def connect; end + def connected?; true; end + + def read + case $subpacket_index + when 0 + "\x01\x05" # Normal packet PKT1 + when 1 + "\x0A\x06" # Subpacket SUB1 + when 2 + "\x63\x07" # Virtual packet (should not be identified) + else + "\xFF\xFF" # Unknown + end + end + end + + it "identifies normal packets but not subpackets" do + @interface.add_protocol(FixedProtocol, [2, 0, nil, true], :READ) + @interface.instance_variable_set(:@stream, SubpacketStream.new) + @interface.target_names = ['TEST'] + @interface.tlm_target_names = ['TEST'] + + $subpacket_index = 0 + packet = @interface.read + expect(packet.target_name).to eql "TEST" + expect(packet.packet_name).to eql "PKT1" + end + + it "does not identify subpackets at interface level" do + @interface.add_protocol(FixedProtocol, [2, 0, nil, true], :READ) + @interface.instance_variable_set(:@stream, SubpacketStream.new) + @interface.target_names = ['TEST'] + @interface.tlm_target_names = ['TEST'] + + $subpacket_index = 1 + packet = @interface.read + expect(packet.target_name).to be_nil + expect(packet.packet_name).to be_nil + end + + it "does not identify virtual packets" do + @interface.add_protocol(FixedProtocol, [2, 0, nil, true], :READ) + @interface.instance_variable_set(:@stream, SubpacketStream.new) + @interface.target_names = ['TEST'] + @interface.tlm_target_names = ['TEST'] + + $subpacket_index = 2 + packet = @interface.read + expect(packet.target_name).to be_nil + expect(packet.packet_name).to be_nil + end + end end end diff --git a/openc3/spec/packets/commands_spec.rb b/openc3/spec/packets/commands_spec.rb index 86938113ad..9c3b9a19f2 100644 --- a/openc3/spec/packets/commands_spec.rb +++ b/openc3/spec/packets/commands_spec.rb @@ -483,5 +483,76 @@ module OpenC3 expect(@cmd.all.keys).to eql %w(UNKNOWN TGT1 TGT2) end end + + describe "identify with subpackets" do + before(:each) do + tf = Tempfile.new('unittest') + tf.puts 'COMMAND tgt3 pkt1 LITTLE_ENDIAN "Normal Command"' + tf.puts ' APPEND_ID_PARAMETER item1 8 UINT 1 1 1 "Item1"' + tf.puts ' APPEND_PARAMETER item2 8 UINT 0 255 2 "Item2"' + tf.puts 'COMMAND tgt3 sub1 LITTLE_ENDIAN "Subcommand 1"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_PARAMETER item1 8 UINT 10 10 10 "Item1"' + tf.puts ' APPEND_PARAMETER item2 8 UINT 0 255 2 "Item2"' + tf.puts 'COMMAND tgt3 sub2 LITTLE_ENDIAN "Subcommand 2"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_PARAMETER item1 8 UINT 20 20 20 "Item1"' + tf.puts ' APPEND_PARAMETER item2 8 UINT 0 255 3 "Item2"' + tf.close + + pc = PacketConfig.new + pc.process_file(tf.path, "TGT3") + @cmd3 = Commands.new(pc) + tf.unlink + end + + it "identifies normal packet when subpackets: false" do + buffer = "\x01\x02" + pkt = @cmd3.identify(buffer, ["TGT3"], subpackets: false) + expect(pkt).to_not be_nil + expect(pkt.packet_name).to eql "PKT1" + end + + it "does not identify subpacket when subpackets: false" do + buffer = "\x0A\x02" + pkt = @cmd3.identify(buffer, ["TGT3"], subpackets: false) + expect(pkt).to be_nil + end + + it "identifies subpacket when subpackets: true" do + buffer = "\x0A\x02" + pkt = @cmd3.identify(buffer, ["TGT3"], subpackets: true) + expect(pkt).to_not be_nil + expect(pkt.packet_name).to eql "SUB1" + end + + it "does not identify normal packet when subpackets: true" do + buffer = "\x01\x02" + pkt = @cmd3.identify(buffer, ["TGT3"], subpackets: true) + expect(pkt).to be_nil + end + + it "identifies different subpackets" do + buffer = "\x14\x03" + pkt = @cmd3.identify(buffer, ["TGT3"], subpackets: true) + expect(pkt).to_not be_nil + expect(pkt.packet_name).to eql "SUB2" + end + end + + describe "cmd_unique_id_mode" do + it "returns unique_id_mode for target" do + expect(@cmd.cmd_unique_id_mode("TGT1")).to be_falsey + # TGT2 has pkt6 and pkt7 with same ID but different bit sizes, triggering unique_id_mode + expect(@cmd.cmd_unique_id_mode("TGT2")).to be_truthy + end + end + + describe "cmd_subpacket_unique_id_mode" do + it "returns subpacket unique_id_mode for target" do + expect(@cmd.cmd_subpacket_unique_id_mode("TGT1")).to be_falsey + expect(@cmd.cmd_subpacket_unique_id_mode("TGT2")).to be_falsey + end + end end end diff --git a/openc3/spec/packets/packet_config_spec.rb b/openc3/spec/packets/packet_config_spec.rb index 9516e19f4b..d647c9aa94 100644 --- a/openc3/spec/packets/packet_config_spec.rb +++ b/openc3/spec/packets/packet_config_spec.rb @@ -26,6 +26,20 @@ require 'tempfile' module OpenC3 + # Test subpacketizer class for unit tests + class TestSubpacketizer + attr_reader :args + + def initialize(packet, *args) + @packet = packet + @args = args + end + + def call(packet) + [packet] + end + end + describe PacketConfig do describe "process_file" do before(:all) do @@ -1225,6 +1239,95 @@ module OpenC3 tf.unlink end end + + context "with SUBPACKET" do + it "marks packet as subpacket" do + tf = Tempfile.new('unittest') + tf.puts 'TELEMETRY tgt1 pkt1 LITTLE_ENDIAN "Packet"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 1 "Item1"' + tf.close + @pc.process_file(tf.path, "TGT1") + expect(@pc.telemetry["TGT1"]["PKT1"].subpacket).to be true + tf.unlink + end + + it "builds subpacket ID hash for telemetry" do + tf = Tempfile.new('unittest') + tf.puts 'TELEMETRY tgt1 pkt1 LITTLE_ENDIAN "Normal Packet"' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 1 "Item1"' + tf.puts 'TELEMETRY tgt1 sub1 LITTLE_ENDIAN "Subpacket 1"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 10 "Item1"' + tf.puts 'TELEMETRY tgt1 sub2 LITTLE_ENDIAN "Subpacket 2"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 20 "Item1"' + tf.close + @pc.process_file(tf.path, "TGT1") + expect(@pc.tlm_id_value_hash["TGT1"].keys).to eql([[1]]) + expect(@pc.tlm_subpacket_id_value_hash["TGT1"].keys).to contain_exactly([10], [20]) + expect(@pc.tlm_subpacket_id_value_hash["TGT1"][[10]].packet_name).to eql("SUB1") + expect(@pc.tlm_subpacket_id_value_hash["TGT1"][[20]].packet_name).to eql("SUB2") + tf.unlink + end + + it "builds subpacket ID hash for commands" do + tf = Tempfile.new('unittest') + tf.puts 'COMMAND tgt1 pkt1 LITTLE_ENDIAN "Normal Command"' + tf.puts ' APPEND_ID_PARAMETER item1 8 UINT 1 1 1 "Item1"' + tf.puts 'COMMAND tgt1 sub1 LITTLE_ENDIAN "Subcommand 1"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_PARAMETER item1 8 UINT 10 10 10 "Item1"' + tf.puts 'COMMAND tgt1 sub2 LITTLE_ENDIAN "Subcommand 2"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_PARAMETER item1 8 UINT 20 20 20 "Item1"' + tf.close + @pc.process_file(tf.path, "TGT1") + expect(@pc.cmd_id_value_hash["TGT1"].keys).to eql([[1]]) + expect(@pc.cmd_subpacket_id_value_hash["TGT1"].keys).to contain_exactly([10], [20]) + expect(@pc.cmd_subpacket_id_value_hash["TGT1"][[10]].packet_name).to eql("SUB1") + expect(@pc.cmd_subpacket_id_value_hash["TGT1"][[20]].packet_name).to eql("SUB2") + tf.unlink + end + + it "detects unique_id_mode for subpackets" do + tf = Tempfile.new('unittest') + tf.puts 'TELEMETRY tgt1 sub1 LITTLE_ENDIAN "Subpacket 1"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 10 "Item1"' + tf.puts 'TELEMETRY tgt1 sub2 LITTLE_ENDIAN "Subpacket 2"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_ITEM item1 16 UINT 20 "Item1"' + tf.close + @pc.process_file(tf.path, "TGT1") + expect(@pc.tlm_subpacket_unique_id_mode["TGT1"]).to be true + tf.unlink + end + end + + context "with SUBPACKETIZER" do + it "sets subpacketizer on telemetry packet" do + tf = Tempfile.new('unittest') + tf.puts 'TELEMETRY tgt1 pkt1 LITTLE_ENDIAN "Packet"' + tf.puts ' SUBPACKETIZER TestSubpacketizer' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 1 "Item1"' + tf.close + @pc.process_file(tf.path, "TGT1") + expect(@pc.telemetry["TGT1"]["PKT1"].subpacketizer).to_not be_nil + tf.unlink + end + + it "sets subpacketizer on command packet" do + tf = Tempfile.new('unittest') + tf.puts 'COMMAND tgt1 pkt1 LITTLE_ENDIAN "Packet"' + tf.puts ' SUBPACKETIZER TestSubpacketizer' + tf.puts ' APPEND_ID_PARAMETER item1 8 UINT 1 1 1 "Item1"' + tf.close + @pc.process_file(tf.path, "TGT1") + expect(@pc.commands["TGT1"]["PKT1"].subpacketizer).to_not be_nil + tf.unlink + end + end end end end diff --git a/openc3/spec/packets/packet_spec.rb b/openc3/spec/packets/packet_spec.rb index 80139f36e5..d1e0951cd4 100644 --- a/openc3/spec/packets/packet_spec.rb +++ b/openc3/spec/packets/packet_spec.rb @@ -1926,5 +1926,58 @@ module OpenC3 expect(p.buffer).to eql "\x01\x02\x03\x04\x00" end end + + describe "subpacketize" do + it "returns array with single packet when no subpacketizer" do + p = Packet.new("tgt", "pkt") + p.append_item("item1", 8, :UINT) + p.buffer = "\x01" + result = p.subpacketize + expect(result).to be_a(Array) + expect(result.length).to eql 1 + expect(result[0]).to eql p + end + + it "calls subpacketizer when present" do + p = Packet.new("tgt", "pkt") + p.append_item("item1", 8, :UINT) + p.buffer = "\x01" + + subpacketizer = double("subpacketizer") + expect(subpacketizer).to receive(:call).with(p).and_return([p, p.clone]) + p.subpacketizer = subpacketizer + + result = p.subpacketize + expect(result).to be_a(Array) + expect(result.length).to eql 2 + end + end + + describe "subpacket attribute" do + it "initializes to false" do + p = Packet.new("tgt", "pkt") + expect(p.subpacket).to eql false + end + + it "can be set to true" do + p = Packet.new("tgt", "pkt") + p.subpacket = true + expect(p.subpacket).to eql true + end + end + + describe "subpacketizer attribute" do + it "initializes to nil" do + p = Packet.new("tgt", "pkt") + expect(p.subpacketizer).to eql nil + end + + it "can be set to a subpacketizer object" do + p = Packet.new("tgt", "pkt") + subpacketizer = double("subpacketizer") + p.subpacketizer = subpacketizer + expect(p.subpacketizer).to eql subpacketizer + end + end end end diff --git a/openc3/spec/packets/telemetry_spec.rb b/openc3/spec/packets/telemetry_spec.rb index 79fc49253f..753bac7f69 100644 --- a/openc3/spec/packets/telemetry_spec.rb +++ b/openc3/spec/packets/telemetry_spec.rb @@ -633,5 +633,83 @@ module OpenC3 end end end + + describe "identify with subpackets" do + before(:each) do + tf = Tempfile.new('unittest') + tf.puts 'TELEMETRY tgt3 pkt1 LITTLE_ENDIAN "Normal Telemetry"' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 1 "Item1"' + tf.puts ' APPEND_ITEM item2 8 UINT "Item2"' + tf.puts 'TELEMETRY tgt3 sub1 LITTLE_ENDIAN "Subpacket 1"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 10 "Item1"' + tf.puts ' APPEND_ITEM item2 8 UINT "Item2"' + tf.puts 'TELEMETRY tgt3 sub2 LITTLE_ENDIAN "Subpacket 2"' + tf.puts ' SUBPACKET' + tf.puts ' APPEND_ID_ITEM item1 8 UINT 20 "Item1"' + tf.puts ' APPEND_ITEM item2 8 UINT "Item2"' + tf.close + + pc = PacketConfig.new + pc.process_file(tf.path, "TGT3") + @tlm3 = Telemetry.new(pc) + tf.unlink + end + + it "identifies normal packet when subpackets: false" do + buffer = "\x01\x02" + pkt = @tlm3.identify(buffer, ["TGT3"], subpackets: false) + expect(pkt).to_not be_nil + expect(pkt.packet_name).to eql "PKT1" + end + + it "does not identify subpacket when subpackets: false" do + buffer = "\x0A\x02" + pkt = @tlm3.identify(buffer, ["TGT3"], subpackets: false) + expect(pkt).to be_nil + end + + it "identifies subpacket when subpackets: true" do + buffer = "\x0A\x02" + pkt = @tlm3.identify(buffer, ["TGT3"], subpackets: true) + expect(pkt).to_not be_nil + expect(pkt.packet_name).to eql "SUB1" + end + + it "does not identify normal packet when subpackets: true" do + buffer = "\x01\x02" + pkt = @tlm3.identify(buffer, ["TGT3"], subpackets: true) + expect(pkt).to be_nil + end + + it "identifies different subpackets" do + buffer = "\x14\x03" + pkt = @tlm3.identify(buffer, ["TGT3"], subpackets: true) + expect(pkt).to_not be_nil + expect(pkt.packet_name).to eql "SUB2" + end + + it "identifies! sets buffer on subpacket" do + buffer = "\x0A\x05" + pkt = @tlm3.identify!(buffer, ["TGT3"], subpackets: true) + expect(pkt).to_not be_nil + expect(pkt.packet_name).to eql "SUB1" + expect(pkt.buffer).to eql buffer + end + end + + describe "tlm_unique_id_mode" do + it "returns unique_id_mode for target" do + expect(@tlm.tlm_unique_id_mode("TGT1")).to be_falsey + expect(@tlm.tlm_unique_id_mode("TGT2")).to be_falsey + end + end + + describe "tlm_subpacket_unique_id_mode" do + it "returns subpacket unique_id_mode for target" do + expect(@tlm.tlm_subpacket_unique_id_mode("TGT1")).to be_falsey + expect(@tlm.tlm_subpacket_unique_id_mode("TGT2")).to be_falsey + end + end end end From 11ca8c38fcf7de4482d252247341b78a54b6dd6c Mon Sep 17 00:00:00 2001 From: Ryan Melton Date: Wed, 22 Oct 2025 20:23:06 -0600 Subject: [PATCH 04/14] python updates --- .../lib/openc3/accessors/template_accessor.rb | 1 + .../openc3/accessors/template_accessor.py | 1 + .../interfaces/protocols/fixed_protocol.py | 38 ++-- .../microservices/decom_microservice.py | 47 +++-- openc3/python/openc3/models/target_model.py | 2 - .../openc3/packets/command_validator.py | 3 - openc3/python/openc3/packets/commands.py | 43 ++++- openc3/python/openc3/packets/packet.py | 24 ++- openc3/python/openc3/packets/packet_config.py | 175 ++++++++++++------ openc3/python/openc3/packets/telemetry.py | 49 +++-- .../python/openc3/subpacketizers/__init__.py | 19 ++ .../openc3/subpacketizers/subpacketizer.py | 50 +++++ openc3/python/openc3/system/target.py | 18 +- .../protocols/test_fixed_protocol.py | 103 ++++++++++- openc3/python/test/packets/test_commands.py | 103 ++++++++++- openc3/python/test/packets/test_packet.py | 47 +++++ .../python/test/packets/test_packet_config.py | 87 +++++++++ openc3/python/test/packets/test_telemetry.py | 105 +++++++++++ 18 files changed, 782 insertions(+), 133 deletions(-) create mode 100644 openc3/python/openc3/subpacketizers/__init__.py create mode 100644 openc3/python/openc3/subpacketizers/subpacketizer.py diff --git a/openc3/lib/openc3/accessors/template_accessor.rb b/openc3/lib/openc3/accessors/template_accessor.rb index a38fd3b419..607d2c44a6 100644 --- a/openc3/lib/openc3/accessors/template_accessor.rb +++ b/openc3/lib/openc3/accessors/template_accessor.rb @@ -25,6 +25,7 @@ def initialize(packet, left_char = '<', right_char = '>') @left_char = left_char @right_char = right_char @configured = false + @args = [left_char, right_char] end def configure diff --git a/openc3/python/openc3/accessors/template_accessor.py b/openc3/python/openc3/accessors/template_accessor.py index 052de7e038..0ddf3ef64d 100644 --- a/openc3/python/openc3/accessors/template_accessor.py +++ b/openc3/python/openc3/accessors/template_accessor.py @@ -24,6 +24,7 @@ def __init__(self, packet, left_char="<", right_char=">"): self.left_char = left_char self.right_char = right_char self.configured = False + self.args = [left_char, right_char] def configure(self): if self.configured: diff --git a/openc3/python/openc3/interfaces/protocols/fixed_protocol.py b/openc3/python/openc3/interfaces/protocols/fixed_protocol.py index e2e3fe1ee2..87412ed867 100644 --- a/openc3/python/openc3/interfaces/protocols/fixed_protocol.py +++ b/openc3/python/openc3/interfaces/protocols/fixed_protocol.py @@ -82,14 +82,10 @@ def identify_and_finish_packet(self): try: if self.telemetry: target_packets = System.telemetry.packets(target_name) - target = System.targets[target_name] - if target: - unique_id_mode = target.tlm_unique_id_mode + unique_id_mode = System.telemetry.tlm_unique_id_mode(target_name) else: target_packets = System.commands.packets(target_name) - target = System.targets[target_name] - if target: - unique_id_mode = target.cmd_unique_id_mode + unique_id_mode = System.commands.cmd_unique_id_mode(target_name) except RuntimeError as error: if "does not exist" in traceback.format_exc(): # No commands/telemetry for this target @@ -99,21 +95,31 @@ def identify_and_finish_packet(self): if unique_id_mode: for _, packet in target_packets.items(): - if packet.identify(self.data[self.discard_leading_bytes :]): + if not packet.subpacket and packet.identify( + self.data[self.discard_leading_bytes :] + ): # identify handles virtual identified_packet = packet break else: # Do a lookup to quickly identify the packet if len(target_packets) > 0: - packet = next(iter(target_packets.values())) - key = packet.read_id_values(self.data[self.discard_leading_bytes :]) - if self.telemetry: - id_values = System.telemetry.config.tlm_id_value_hash[target_name] - else: - id_values = System.commands.config.cmd_id_value_hash[target_name] - identified_packet = id_values.get(repr(key)) - if identified_packet is None: - identified_packet = id_values.get("CATCHALL") + packet = None + for _, target_packet in target_packets.items(): + if target_packet.virtual: + continue + if target_packet.subpacket: + continue + packet = target_packet + break + if packet: + key = packet.read_id_values(self.data[self.discard_leading_bytes :]) + if self.telemetry: + id_values = System.telemetry.config.tlm_id_value_hash[target_name] + else: + id_values = System.commands.config.cmd_id_value_hash[target_name] + identified_packet = id_values.get(repr(key)) + if identified_packet is None: + identified_packet = id_values.get("CATCHALL") if identified_packet is not None: if identified_packet.defined_length + self.discard_leading_bytes > len(self.data): diff --git a/openc3/python/openc3/microservices/decom_microservice.py b/openc3/python/openc3/microservices/decom_microservice.py index 87fefe90aa..6352dc5ac5 100644 --- a/openc3/python/openc3/microservices/decom_microservice.py +++ b/openc3/python/openc3/microservices/decom_microservice.py @@ -168,9 +168,12 @@ def decom_packet(self, topic, msg_id, msg_hash, _redis): ) start = time.time() + + ####################################### + # Build packet object from topic data + ####################################### target_name = msg_hash[b"target_name"].decode() packet_name = msg_hash[b"packet_name"].decode() - packet = System.telemetry.packet(target_name, packet_name) packet.stored = ConfigParser.handle_true_false(msg_hash[b"stored"].decode()) # Note: Packet time will be recalculated as part of decom so not setting @@ -180,22 +183,34 @@ def decom_packet(self, topic, msg_id, msg_hash, _redis): if extra is not None: packet.extra = json.loads(extra) packet.buffer = msg_hash[b"buffer"] - # Processors are user code points which must be rescued - # so the TelemetryDecomTopic can write the packet - try: - packet.process() # Run processors - except Exception as error: - self.error_count += 1 - self.metric.set(name="decom_error_total", value=self.error_count, type="counter") - self.error = error - self.logger.error(f"Processor error:\n{traceback.format_exc()}") - # Process all the limits and call the limits_change_callback (as necessary) - # check_limits also can call user code in the limits response - # but that is rescued separately in the limits_change_callback - packet.check_limits(System.limits_set()) - # This is what updates the CVT - TelemetryDecomTopic.write_packet(packet, scope=self.scope) + ################################################################################ + # Break packet into subpackets (if necessary) + # Subpackets are typically channelized data + ################################################################################ + subpackets = packet.subpacketize() + + for subpacket in subpackets: + ##################################################################################### + # Run Processors + # This must be before the full decom so that processor derived values are available + ##################################################################################### + try: + subpacket.process() # Run processors + except Exception as error: + self.error_count += 1 + self.metric.set(name="decom_error_total", value=self.error_count, type="counter") + self.error = error + self.logger.error(f"Processor error:\n{traceback.format_exc()}") + + ############################################################################# + # Process all the limits and call the limits_change_callback (as necessary) + # This must be before the full decom so that limits states are available + ############################################################################# + subpacket.check_limits(System.limits_set()) + + # This is what actually decommutates the packet and updates the CVT + TelemetryDecomTopic.write_packet(subpacket, scope=self.scope) diff = time.time() - start # seconds as a float self.metric.set(name="decom_duration_seconds", value=diff, type="gauge", unit="seconds") diff --git a/openc3/python/openc3/models/target_model.py b/openc3/python/openc3/models/target_model.py index 0b4e6fec37..f5c92d0d2b 100644 --- a/openc3/python/openc3/models/target_model.py +++ b/openc3/python/openc3/models/target_model.py @@ -347,8 +347,6 @@ def __init__( ignored_items=[], limits_groups=[], cmd_tlm_files=[], - cmd_unique_id_mode=False, - tlm_unique_id_mode=False, id=None, updated_at=None, plugin=None, diff --git a/openc3/python/openc3/packets/command_validator.py b/openc3/python/openc3/packets/command_validator.py index 50153af742..0786459947 100644 --- a/openc3/python/openc3/packets/command_validator.py +++ b/openc3/python/openc3/packets/command_validator.py @@ -34,6 +34,3 @@ def post_check(self, command): # Return True to indicate Success, False to indicate Failure, # and None to indicate Unknown. The second value is the optional message. return [True, None] - - def args(self): - return self.args diff --git a/openc3/python/openc3/packets/commands.py b/openc3/python/openc3/packets/commands.py index 9596de0606..663e17cb07 100644 --- a/openc3/python/openc3/packets/commands.py +++ b/openc3/python/openc3/packets/commands.py @@ -92,7 +92,7 @@ def params(self, target_name, packet_name): # # @param (see #identify_tlm!) # @return (see #identify_tlm!) - def identify(self, packet_data, target_names=None): + def identify(self, packet_data, target_names=None, subpackets=False): identified_packet = None if target_names is None: @@ -107,20 +107,41 @@ def identify(self, packet_data, target_names=None): # No commands for this target continue - target = self.system.targets.get(target_name) - if target and target.cmd_unique_id_mode: + if (not subpackets and self.cmd_unique_id_mode(target_name)) or ( + subpackets and self.cmd_subpacket_unique_id_mode(target_name) + ): # Iterate through the packets and see if any represent the buffer for _, packet in target_packets.items(): - if packet.identify(packet_data): + if subpackets: + if not packet.subpacket: + continue + else: + if packet.subpacket: + continue + if packet.identify(packet_data): # Handles virtual identified_packet = packet break else: # Do a lookup to quickly identify the packet - if len(target_packets) > 0: - packet = next(iter(target_packets.values())) + packet = None + for _, target_packet in target_packets.items(): + if target_packet.virtual: + continue + if subpackets: + if not target_packet.subpacket: + continue + else: + if target_packet.subpacket: + continue + packet = target_packet + break + if packet: key = packet.read_id_values(packet_data) - id_values = self.config.cmd_id_value_hash[target_name] - identified_packet = id_values.get(str(key)) + if subpackets: + id_values = self.config.cmd_subpacket_id_value_hash[target_name] + else: + id_values = self.config.cmd_id_value_hash[target_name] + identified_packet = id_values.get(repr(key)) if identified_packet is None: identified_packet = id_values.get("CATCHALL") @@ -248,6 +269,12 @@ def all(self): def dynamic_add_packet(self, packet, affect_ids=False): self.config.dynamic_add_packet(packet, "COMMAND", affect_ids=affect_ids) + def cmd_unique_id_mode(self, target_name): + return self.config.cmd_unique_id_mode.get(target_name.upper()) + + def cmd_subpacket_unique_id_mode(self, target_name): + return self.config.cmd_subpacket_unique_id_mode.get(target_name.upper()) + def _set_parameters(self, command, params, range_checking): given_item_names = [] for item_name, value in params.items(): diff --git a/openc3/python/openc3/packets/packet.py b/openc3/python/openc3/packets/packet.py index 35e8393fc6..61ddb0c205 100644 --- a/openc3/python/openc3/packets/packet.py +++ b/openc3/python/openc3/packets/packet.py @@ -102,7 +102,9 @@ def __init__( self.ignore_overlap = False self.virtual = False self.restricted = False + self.subpacket = False self.validator = None + self.subpacketizer = None self.obfuscated_items = [] self.obfuscated_items_hash = {} @@ -1010,10 +1012,15 @@ def to_config(self, cmd_or_tlm): config += f'TELEMETRY {quote_if_necessary(self.target_name)} {quote_if_necessary(self.packet_name)} {self.default_endianness} "{self.description}"\n' else: config += f'COMMAND {quote_if_necessary(self.target_name)} {quote_if_necessary(self.packet_name)} {self.default_endianness} "{self.description}"\n' + if self.subpacketizer: + args_str = " ".join([quote_if_necessary(str(a)) for a in self.subpacketizer.args]) + config += f" SUBPACKETIZER {self.subpacketizer.__class__.__name__} {args_str}\n" if self.accessor.__class__.__name__ != "BinaryAccessor": - config += f" ACCESSOR {self.accessor.__class__.__name__}\n" + args_str = " ".join([quote_if_necessary(str(a)) for a in self.accessor.args]) + config += f" ACCESSOR {self.accessor.__class__.__name__} {args_str}\n" if self.validator: - config += f" VALIDATOR {self.validator.__class__.__name__}\n" + args_str = " ".join([quote_if_necessary(str(a)) for a in self.validator.args]) + config += f" VALIDATOR {self.validator.__class__.__name__} {args_str}\n" # TODO: Add TEMPLATE_ENCODED so this can always be done inline regardless of content if self.template: config += f" TEMPLATE '{self.template}'\n" @@ -1029,6 +1036,8 @@ def to_config(self, cmd_or_tlm): config += " DISABLED\n" elif self.hidden: config += " HIDDEN\n" + if self.subpacket: + config += " SUBPACKET\n" if self.restricted: config += " RESTRICTED\n" @@ -1317,3 +1326,14 @@ def obfuscate(self): except Exception as e: Logger.error(f"{item.name} obfuscation failed with error: {repr(e)}") continue + + def subpacketize(self): + """Break packet into subpackets using subpacketizer if defined. + + Returns: + list: List of packet objects (subpackets or [self] if no subpacketizer) + """ + if self.subpacketizer: + return self.subpacketizer.call(self) + else: + return [self] diff --git a/openc3/python/openc3/packets/packet_config.py b/openc3/python/openc3/packets/packet_config.py index bb81b90be8..86fa0cbf78 100644 --- a/openc3/python/openc3/packets/packet_config.py +++ b/openc3/python/openc3/packets/packet_config.py @@ -58,7 +58,17 @@ def __init__(self): self.latest_data = {} self.warnings = [] self.cmd_id_value_hash = {} + self.cmd_subpacket_id_value_hash = {} + self.cmd_id_signature = {} + self.cmd_subpacket_id_signature = {} + self.cmd_unique_id_mode = {} + self.cmd_subpacket_unique_id_mode = {} self.tlm_id_value_hash = {} + self.tlm_subpacket_id_value_hash = {} + self.tlm_id_signature = {} + self.tlm_subpacket_id_signature = {} + self.tlm_unique_id_mode = {} + self.tlm_subpacket_unique_id_mode = {} # Create unknown packets self.commands["UNKNOWN"] = {} @@ -211,9 +221,11 @@ def process_file(self, filename, process_target_name): | "DISABLE_MESSAGES" | "HIDDEN" | "DISABLED" + | "SUBPACKET" | "VIRTUAL" | "ACCESSOR" | "VALIDATOR" + | "SUBPACKETIZER" | "TEMPLATE" | "TEMPLATE_FILE" | "RESPONSE" @@ -329,19 +341,37 @@ def finish_packet(self): PacketParser.check_item_data_types(self.current_packet) self.commands[self.current_packet.target_name][self.current_packet.packet_name] = self.current_packet if not self.current_packet.virtual: - id_values = self.cmd_id_value_hash.get(self.current_packet.target_name) - if not id_values: - id_values = {} - self.cmd_id_value_hash[self.current_packet.target_name] = id_values - self.update_id_value_hash(self.current_packet, id_values) + if self.current_packet.subpacket: + self.build_id_metadata( + self.current_packet, + self.cmd_subpacket_id_value_hash, + self.cmd_subpacket_id_signature, + self.cmd_subpacket_unique_id_mode, + ) + else: + self.build_id_metadata( + self.current_packet, + self.cmd_id_value_hash, + self.cmd_id_signature, + self.cmd_unique_id_mode, + ) else: self.telemetry[self.current_packet.target_name][self.current_packet.packet_name] = self.current_packet if not self.current_packet.virtual: - id_values = self.tlm_id_value_hash.get(self.current_packet.target_name) - if not id_values: - id_values = {} - self.tlm_id_value_hash[self.current_packet.target_name] = id_values - self.update_id_value_hash(self.current_packet, id_values) + if self.current_packet.subpacket: + self.build_id_metadata( + self.current_packet, + self.tlm_subpacket_id_value_hash, + self.tlm_subpacket_id_signature, + self.tlm_subpacket_unique_id_mode, + ) + else: + self.build_id_metadata( + self.current_packet, + self.tlm_id_value_hash, + self.tlm_id_signature, + self.tlm_unique_id_mode, + ) self.current_packet = None self.current_item = None @@ -350,12 +380,18 @@ def dynamic_add_packet(self, packet, cmd_or_tlm="TELEMETRY", affect_ids=False): if cmd_or_tlm == "COMMAND": self.commands[packet.target_name][packet.packet_name] = packet - if affect_ids: - id_values = self.cmd_id_value_hash.get(packet.target_name, None) - if not id_values: - id_values = {} - self.cmd_id_value_hash[packet.target_name] = id_values - self.update_id_value_hash(packet, id_values) + if affect_ids and not packet.virtual: + if packet.subpacket: + self.build_id_metadata( + packet, + self.cmd_subpacket_id_value_hash, + self.cmd_subpacket_id_signature, + self.cmd_subpacket_unique_id_mode, + ) + else: + self.build_id_metadata( + packet, self.cmd_id_value_hash, self.cmd_id_signature, self.cmd_unique_id_mode + ) else: self.telemetry[packet.target_name][packet.packet_name] = packet @@ -368,12 +404,58 @@ def dynamic_add_packet(self, packet, cmd_or_tlm="TELEMETRY", affect_ids=False): if packet not in latest_data_packets: latest_data_packets.append(packet) - if affect_ids: - id_values = self.tlm_id_value_hash.get(packet.target_name, None) - if not id_values: - id_values = {} - self.tlm_id_value_hash[packet.target_name] = id_values - self.update_id_value_hash(packet, id_values) + if affect_ids and not packet.virtual: + if packet.subpacket: + self.build_id_metadata( + packet, + self.tlm_subpacket_id_value_hash, + self.tlm_subpacket_id_signature, + self.tlm_subpacket_unique_id_mode, + ) + else: + self.build_id_metadata( + packet, self.tlm_id_value_hash, self.tlm_id_signature, self.tlm_unique_id_mode + ) + + def build_id_metadata(self, packet, id_value_hash, id_signature_hash, unique_id_mode_hash): + """Build identification metadata for a packet. + + Args: + packet: The packet to build metadata for + id_value_hash: Hash mapping target names to ID value hashes + id_signature_hash: Hash mapping target names to ID signatures + unique_id_mode_hash: Hash mapping target names to unique ID mode flags + """ + target_id_value_hash = id_value_hash.get(packet.target_name) + if not target_id_value_hash: + target_id_value_hash = {} + id_value_hash[packet.target_name] = target_id_value_hash + self.update_id_value_hash(packet, target_id_value_hash, id_signature_hash, unique_id_mode_hash) + + def update_id_value_hash(self, packet, target_id_value_hash, id_signature_hash, unique_id_mode_hash): + """Update the ID value hash for a packet and track ID signatures. + + Args: + packet: The packet to update + target_id_value_hash: Hash mapping ID values to packets for this target + id_signature_hash: Hash mapping target names to ID signatures + unique_id_mode_hash: Hash mapping target names to unique ID mode flags + """ + if packet.id_items and len(packet.id_items) > 0: + key = [] + id_signature = "" + for item in packet.id_items: + key.append(item.id_value) + id_signature += f"__{item.name}___{item.bit_offset}__{item.bit_size}__{item.data_type}" + target_id_value_hash[repr(key)] = packet + target_id_signature = id_signature_hash.get(packet.target_name) + if target_id_signature: + if id_signature != target_id_signature: + unique_id_mode_hash[packet.target_name] = True + else: + id_signature_hash[packet.target_name] = id_signature + else: + target_id_value_hash["CATCHALL"] = packet # This method provides way to quickly test packet configs # @@ -397,16 +479,6 @@ def from_config(cls, config, process_target_name): pc.process_file(tf.name, process_target_name) return pc - def update_id_value_hash(self, packet, hash): - if packet.id_items and len(packet.id_items) > 0: - key = [] - for item in packet.id_items: - key.append(item.id_value) - - hash[repr(key)] = packet - else: - hash["CATCHALL"] = packet - def reset_processing_variables(self): self.current_cmd_or_tlm = None self.current_packet = None @@ -511,12 +583,17 @@ def process_current_packet(self, parser, keyword, params): self.current_packet.disabled = True self.current_packet.virtual = True + case "SUBPACKET": + usage = keyword + parser.verify_num_parameters(0, 0, usage) + self.current_packet.subpacket = True + case "RESTRICTED": usage = keyword parser.verify_num_parameters(0, 0, usage) self.current_packet.restricted = True - case "ACCESSOR": + case "ACCESSOR" | "VALIDATOR" | "SUBPACKETIZER": usage = f"{keyword} ..." parser.verify_num_parameters(1, None, usage) klass = None @@ -527,30 +604,22 @@ def process_current_packet(self, parser, keyword, params): ) except ModuleNotFoundError: try: - # Fall back to the deprecated behavior of passing the ClassName (only works with built-in accessors) + # Fall back to the deprecated behavior of passing the ClassName filename = class_name_to_filename(params[0]) - klass = get_class_from_module(f"openc3.accessors.{filename}", params[0]) + if keyword == "ACCESSOR": + klass = get_class_from_module(f"openc3.accessors.{filename}", params[0]) + elif keyword == "VALIDATOR": + klass = get_class_from_module(f"openc3.validators.{filename}", params[0]) + elif keyword == "SUBPACKETIZER": + klass = get_class_from_module(f"openc3.subpacketizers.{filename}", params[0]) except ModuleNotFoundError: raise parser.error(f"ModuleNotFoundError parsing {params[0]}. Usage: {usage}") + + keyword_attr = keyword.lower() if len(params) > 1: - self.current_packet.accessor = klass(self.current_packet, *params[1:]) + setattr(self.current_packet, keyword_attr, klass(self.current_packet, *params[1:])) else: - self.current_packet.accessor = klass(self.current_packet) - - case "VALIDATOR": - usage = f"{keyword} ..." - parser.verify_num_parameters(1, None, usage) - try: - klass = get_class_from_module( - filename_to_module(params[0]), - filename_to_class_name(params[0]), - ) - if len(params) > 1: - self.current_packet.validator = klass(self.current_packet, *params[1:]) - else: - self.current_packet.validator = klass(self.current_packet) - except ModuleNotFoundError as error: - raise parser.error(error) + setattr(self.current_packet, keyword_attr, klass(self.current_packet)) case "TEMPLATE": usage = f"{keyword}