# encoding: ascii-8bit # Copyright 2014 Ball Aerospace & Technologies Corp. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it # under the terms of the GNU General Public License # as published by the Free Software Foundation; version 3 with # attribution addendums as found in the LICENSE.txt require 'cosmos/config/config_parser' require 'cosmos/packets/packet_config' require 'cosmos/packets/commands' require 'cosmos/packets/telemetry' require 'cosmos/packets/limits' require 'cosmos/system/target' require 'cosmos/packet_logs' require 'fileutils' require 'drb/acl' require 'zip' require 'zip/filesystem' require 'bundler' module Cosmos # System is the primary entry point into the COSMOS framework. It captures # system wide configuration items such as the available ports and paths to # various files used by the system. The #commands, #telemetry, and #limits # class variables are the primary access points for applications. The # #targets variable provides access to all the targets defined by the system. # Its primary responsibily is to load the system configuration file and # create all the Target instances. It also saves and restores configurations # using a MD5 checksum over the entire configuration to detect changes. class System # @return [Hash] Hash of all the known ports and their values instance_attr_reader :ports # @return [Hash] Hash of host names or ip addresses for tools to listen on instance_attr_reader :listen_hosts # @return [Hash] Hash of host names or ip addresses for tools to connect to instance_attr_reader :connect_hosts # @return [Hash] Hash of all the known paths and their values instance_attr_reader :paths # @return [PacketLogWriter] Class used to create log files instance_attr_reader :default_packet_log_writer # @return [Array] Parameters to be used with the default log writer instance_attr_reader :default_packet_log_writer_params # @return [PacketLogReader] Class used to read log files instance_attr_reader :default_packet_log_reader # @return [Array] Parameters to be used with the default log reader instance_attr_reader :default_packet_log_reader_params # @return [Boolean] Whether to use sound for alerts instance_attr_reader :sound # @return [Boolean] Whether to use DNS to lookup IP addresses or not instance_attr_reader :use_dns # @return [String] Stores the initial configuration file used when this # System was initialized instance_attr_reader :initial_filename # @return [PacketConfig] Stores the initial packet list used when this # System was initialized instance_attr_reader :initial_config # @return [ACL] Access control list showing which machines can have access instance_attr_reader :acl # @return [Hash] Hash of all the known targets instance_attr_reader :targets # @return [Integer] The number of seconds before a telemetry packet is considered stale instance_attr_reader :staleness_seconds # @return [Symbol] The current limits set instance_attr_reader :limits_set # @return [Boolean] Whether to use UTC or local times instance_attr_reader :use_utc # @return [Array] List of files that are to be included in the MD5 # calculation in addition to the cmd/tlm definition files that are # automatically included instance_attr_reader :additional_md5_files # @return [Hash] Hash of the text/color to use for the classificaiton banner instance_attr_reader :classificiation_banner # Known COSMOS ports KNOWN_PORTS = ['CTS_API', 'TLMVIEWER_API', 'CTS_PREIDENTIFIED', 'CTS_CMD_ROUTER', 'REPLAY_API', 'REPLAY_PREIDENTIFIED', 'REPLAY_CMD_ROUTER', 'DART_STREAM', 'DART_DECOM'] # Known COSMOS hosts KNOWN_HOSTS = ['CTS_API', 'TLMVIEWER_API', 'CTS_PREIDENTIFIED', 'CTS_CMD_ROUTER', 'REPLAY_API', 'REPLAY_PREIDENTIFIED', 'REPLAY_CMD_ROUTER', 'DART_STREAM', 'DART_DECOM'] # Known COSMOS paths KNOWN_PATHS = ['LOGS', 'TMP', 'SAVED_CONFIG', 'TABLES', 'HANDBOOKS', 'PROCEDURES', 'SEQUENCES', 'DART_DATA', 'DART_LOGS'] @@instance = nil @@instance_mutex = Mutex.new # Create a new System object. Note, this should not be called directly but # you should instead use System.instance and treat this class as a # singleton. # # @param filename [String] Full path to the system configuration file to # read. Be default this is /config/system/system.txt def initialize(filename = nil) raise "Cosmos::System created twice" unless @@instance.nil? reset_variables(filename) @@instance = self end # @return [String] Configuration name def self.configuration_name # In order not to make the @config instance variable accessable to the # outside (using a regular accessor method) we use the ability of the # object to grab its own instance variable self.instance.instance_variable_get(:@config).name end # Clear the command and telemetry counters for all the targets as well as # the counters associated with the Commands and Telemetry instances def self.clear_counters self.instance.targets.each do |target_name, target| target.cmd_cnt = 0 target.tlm_cnt = 0 end self.instance.telemetry.clear_counters self.instance.commands.clear_counters end # @return [Commands] Access to the command definiton def commands load_packets() unless @config return @commands end # (see #commands) def self.commands return self.instance.commands end # @return [Telemetry] Access to the telemetry definition def telemetry load_packets() unless @config return @telemetry end # (see #telemetry) def self.telemetry return self.instance.telemetry end # @return [Limits] Access to the limits definition def limits load_packets() unless @config return @limits end # (see #limits) def self.limits return self.instance.limits end # Change the system limits set # # @param limits_set [Symbol] The name of the limits set. :DEFAULT is always an option # but limits sets are user defined def limits_set=(limits_set) load_packets() unless @config new_limits_set = limits_set.to_s.upcase.intern if @limits_set != new_limits_set if @config.limits_sets.include?(new_limits_set) @limits_set = new_limits_set Logger.info("Limits Set Changed to: #{@limits_set}") CmdTlmServer.instance.post_limits_event(:LIMITS_SET, System.limits_set) if defined? CmdTlmServer && CmdTlmServer.instance else raise "Unknown limits set requested: #{new_limits_set}" end end end # (see #limits_set=) def self.limits_set=(limits_set) self.instance.limits_set = limits_set end # @param filename [String] System configuration file to parse # @return [System] The System singleton def self.instance(filename = nil) return @@instance if @@instance @@instance_mutex.synchronize do @@instance ||= self.new(filename) return @@instance end end # Process the system.txt configuration file # # @param filename [String] The configuration file # @param configuration_directory [String] The configuration directory to # search for the target command and telemetry files. Pass nil to look in # the default location of /config/targets. def process_file(filename, configuration_directory = nil) @targets = {} # Set config to nil so things will lazy load later @config = nil @use_utc = false acl_list = [] all_allowed = false first_procedures_path = true @additional_md5_files = [] Cosmos.set_working_dir do parser = ConfigParser.new("http://cosmosrb.com/docs/system") # First pass - Everything except targets parser.parse_file(filename) do |keyword, parameters| case keyword when 'AUTO_DECLARE_TARGETS', 'DECLARE_TARGET', 'DECLARE_GEM_TARGET' # Will be handled by second pass when 'PORT' usage = "#{keyword} " parser.verify_num_parameters(2, 2, usage) port_name = parameters[0].to_s.upcase @ports[port_name] = Integer(parameters[1]) Logger.warn("Unknown port name given: #{port_name}") unless KNOWN_PORTS.include?(port_name) when 'LISTEN_HOST', 'CONNECT_HOST' usage = "#{keyword} " parser.verify_num_parameters(2, 2, usage) host_name = parameters[0].to_s.upcase host = parameters[1] host = '127.0.0.1' if host.to_s.upcase == 'LOCALHOST' if keyword == 'LISTEN_HOST' @listen_hosts[host_name] = host else @connect_hosts[host_name] = host end Logger.warn("Unknown host name given: #{host_name}") unless KNOWN_HOSTS.include?(host_name) when 'PATH' usage = "#{keyword} " parser.verify_num_parameters(2, 2, usage) path_name = parameters[0].to_s.upcase path = File.expand_path(parameters[1]) if path_name == 'PROCEDURES' if first_procedures_path @paths[path_name] = [] first_procedures_path = false end @paths[path_name] << path else @paths[path_name] = path end unless Dir.exist?(path) begin FileUtils.mkdir_p(path) raise "Path creation failed: #{path}" unless File.exist?(path) Logger.info "Created PATH #{path_name} #{path}" rescue Exception => err Logger.error "Problem creating PATH #{path_name} #{path}\n#{err.formatted}" end end Logger.warn("Unknown path name given: #{path_name}") unless KNOWN_PATHS.include?(path_name) when 'DEFAULT_PACKET_LOG_WRITER' usage = "#{keyword} " parser.verify_num_parameters(1, nil, usage) Cosmos.disable_warnings do @default_packet_log_writer = Cosmos.require_class(parameters[0]) end @default_packet_log_writer_params = parameters[1..-1] if parameters.size > 1 when 'DEFAULT_PACKET_LOG_READER' usage = "#{keyword} " parser.verify_num_parameters(1, nil, usage) Cosmos.disable_warnings do @default_packet_log_reader = Cosmos.require_class(parameters[0]) end @default_packet_log_reader_params = parameters[1..-1] if parameters.size > 1 when 'ENABLE_SOUND' usage = "#{keyword}" parser.verify_num_parameters(0, 0, usage) @sound = true when 'DISABLE_DNS' usage = "#{keyword}" parser.verify_num_parameters(0, 0, usage) @use_dns = false when 'ENABLE_DNS' usage = "#{keyword}" parser.verify_num_parameters(0, 0, usage) @use_dns = true when 'ALLOW_ACCESS' parser.verify_num_parameters(1, 1, "#{keyword} ") begin addr = parameters[0].upcase if addr == 'ALL' all_allowed = true acl_list = [] end unless all_allowed first_char = addr[0..0] if !((first_char =~ /[1234567890]/) || (first_char == '*') || (addr.upcase == 'ALL')) # Try to lookup IP Address info = Socket.gethostbyname(addr) addr = "#{info[3].getbyte(0)}.#{info[3].getbyte(1)}.#{info[3].getbyte(2)}.#{info[3].getbyte(3)}" if (acl_list.empty?) acl_list << 'allow' acl_list << '127.0.0.1' end acl_list << 'allow' acl_list << addr else raise "badly formatted address #{addr}" unless /\b(?:\d{1,3}\.){3}\d{1,3}\b/.match(addr) if (acl_list.empty?) acl_list << 'allow' acl_list << '127.0.0.1' end acl_list << 'allow' acl_list << addr end end rescue => err raise parser.error("Problem with ALLOW_ACCESS due to #{err.message.strip}") end when 'STALENESS_SECONDS' parser.verify_num_parameters(1, 1, "#{keyword} ") @staleness_seconds = Integer(parameters[0]) when 'META_INIT' parser.verify_num_parameters(1, 1, "#{keyword} ") @meta_init_filename = ConfigParser.handle_nil(parameters[0]) when 'TIME_ZONE_UTC' parser.verify_num_parameters(0, 0, "#{keyword}") @use_utc = true when 'ADD_MD5_FILE' parser.verify_num_parameters(1, 1, "#{keyword} ") if File.file?(parameters[0]) @additional_md5_files << File.expand_path(parameters[0]) elsif File.file?(File.join(Cosmos::USERPATH, parameters[0])) @additional_md5_files << File.expand_path(File.join(Cosmos::USERPATH, parameters[0])) else raise "Missing expected file: #{parameters[0]}" end when 'CLASSIFICATION' parser.verify_num_parameters(2, 4, "#{keyword} ") # Determine if the COSMOS color already exists, otherwise create a new one if Cosmos.constants.include? parameters[1].upcase.to_sym # We were given a named color that already exists in COSMOS color = eval("Cosmos::#{parameters[1].upcase}") else if parameters.length < 4 # We were given a named color, but it didn't exist in COSMOS already color = Cosmos::getColor(parameters[1].upcase) else # We were given RGB values color = Cosmos::getColor(parameters[1], parameters[2], parameters[3]) end end @classificiation_banner = {'display_text' => parameters[0], 'color' => color} else # blank lines will have a nil keyword and should not raise an exception raise parser.error("Unknown keyword '#{keyword}'") if keyword end # case keyword end # parser.parse_file @acl = ACL.new(acl_list, ACL::ALLOW_DENY) unless acl_list.empty? # Explicitly set up time to use UTC or local if @use_utc Time.use_utc() else Time.use_local() end # Second pass - Process targets process_targets(parser, filename, configuration_directory) end # Cosmos.set_working_dir end # def process_file # Parse the system.txt configuration file looking for keywords associated # with targets and create all the Target instances in the system. # # @param parser [ConfigParser] Parser created by process_file # @param filename (see #process_file) # @param configuration_directory (see #process_file) def process_targets(parser, filename, configuration_directory) parser.parse_file(filename) do |keyword, parameters| case keyword when 'AUTO_DECLARE_TARGETS' usage = "#{keyword}" parser.verify_num_parameters(0, 0, usage) path = File.join(USERPATH, 'config', 'targets') unless File.exist? path raise parser.error("#{path} must exist", usage) end dirs = [] Dir.foreach(File.join(USERPATH, 'config', 'targets')) { |dir_filename| dirs << dir_filename } dirs.sort! dirs.each do |dir_filename| if dir_filename[0] != '.' if dir_filename == dir_filename.upcase # If any of the targets original directory name matches the # current directory then it must have been already processed by # DECLARE_TARGET so we skip it. next if @targets.select {|name, target| target.original_name == dir_filename }.length > 0 next if dir_filename == 'SYSTEM' target = Target.new(dir_filename) @targets[target.name] = target else raise parser.error("Target folder must be uppercase: '#{dir_filename}'") end end end auto_detect_gem_based_targets() # Make sure SYSTEM target is always present and added last target = Target.new('SYSTEM') @targets[target.name] = target when 'DECLARE_TARGET' usage = "#{keyword} " parser.verify_num_parameters(1, 3, usage) target_name = parameters[0].to_s.upcase substitute_name = nil substitute_name = ConfigParser.handle_nil(parameters[1]) substitute_name.to_s.upcase if substitute_name if configuration_directory folder_name = File.join(configuration_directory, target_name) else folder_name = File.join(USERPATH, 'config', 'targets', target_name) end unless Dir.exist?(folder_name) raise parser.error("Target folder must exist '#{folder_name}'.") end target = Target.new(target_name, substitute_name, configuration_directory, ConfigParser.handle_nil(parameters[2])) @targets[target.name] = target when 'DECLARE_GEM_TARGET' usage = "#{keyword} " parser.verify_num_parameters(1, 3, usage) # Remove 'cosmos' from the gem name 'cosmos-power-supply' target_name = parameters[0].split('-')[1..-1].join('-').to_s.upcase substitute_name = nil substitute_name = ConfigParser.handle_nil(parameters[1]) substitute_name.to_s.upcase if substitute_name gem_dir = Gem::Specification.find_by_name(parameters[0]).gem_dir target = Target.new(target_name, substitute_name, configuration_directory, ConfigParser.handle_nil(parameters[2]), gem_dir) @targets[target.name] = target end # case keyword end # parser.parse_file end # Load the specified configuration by iterating through the SAVED_CONFIG # directory looking for a matching MD5 sum. Updates the internal state so # subsequent commands and telemetry methods return the new configuration. # # @param name [String] MD5 string which identifies the # configuration. Pass nil to load the default configuration. # @return [String, Exception/nil] The actual configuration loaded def load_configuration(name = nil) unless @config # Ensure packets have been lazy loaded System.commands end if name && @config # Make sure they're requesting something other than the current # configuration. if name != @config.name # If they want the initial configuration we can just swap out the # current configuration without doing any file processing if name == @initial_config.name update_config(@initial_config) else # Look for the requested configuration in the saved configurations configuration = find_configuration(name) if configuration # We found the configuration requested. Reprocess the system.txt # and reload the packets begin unless File.directory?(configuration) # Zip file configuration so unzip and reset configuration path configuration = unzip(configuration) end process_file(File.join(configuration, 'system.txt'), configuration) load_packets(name) rescue Exception => error # Failed to load - Restore initial update_config(@initial_config) return @config.name, error end else # We couldn't find the configuration request. Reload the # initial configuration update_config(@initial_config) end end end else update_config(@initial_config) end return @config.name, nil end # (see #load_configuration) def self.load_configuration(name = nil) return self.instance.load_configuration(name) end # Resets the System's internal state to defaults. # # @param filename [String] Path to system.txt config file to process. Defaults to config/system/system.txt def reset_variables(filename = nil) @targets = {} @targets['UNKNOWN'] = Target.new('UNKNOWN') @config = nil @commands = nil @telemetry = nil @limits = nil @default_packet_log_writer = PacketLogWriter @default_packet_log_writer_params = [] @default_packet_log_reader = PacketLogReader @default_packet_log_reader_params = [] @sound = false @use_dns = false @acl = nil @staleness_seconds = 30 @limits_set = :DEFAULT @use_utc = false @additional_md5_files = [] @meta_init_filename = nil @ports = {} @ports['CTS_API'] = 7777 @ports['TLMVIEWER_API'] = 7778 @ports['CTS_PREIDENTIFIED'] = 7779 @ports['CTS_CMD_ROUTER'] = 7780 @ports['REPLAY_API'] = 7877 @ports['REPLAY_PREIDENTIFIED'] = 7879 @ports['REPLAY_CMD_ROUTER'] = 7880 @ports['DART_DECOM'] = 8777 @ports['DART_STREAM'] = 8779 @listen_hosts = {} @listen_hosts['CTS_API'] = '127.0.0.1' @listen_hosts['TLMVIEWER_API'] = '127.0.0.1' # Localhost would be more secure but historically these are open to allow for chaining servers by default @listen_hosts['CTS_PREIDENTIFIED'] = '0.0.0.0' @listen_hosts['CTS_CMD_ROUTER'] = '0.0.0.0' @listen_hosts['REPLAY_API'] = '127.0.0.1' # Localhost would be more secure but historically these are open to allow for chaining servers by default @listen_hosts['REPLAY_PREIDENTIFIED'] = '0.0.0.0' @listen_hosts['REPLAY_CMD_ROUTER'] = '0.0.0.0' @listen_hosts['DART_STREAM'] = '0.0.0.0' @listen_hosts['DART_DECOM'] = '0.0.0.0' @connect_hosts = {} @connect_hosts['CTS_API'] = '127.0.0.1' @connect_hosts['TLMVIEWER_API'] = '127.0.0.1' @connect_hosts['CTS_PREIDENTIFIED'] = '127.0.0.1' @connect_hosts['CTS_CMD_ROUTER'] = '127.0.0.1' @connect_hosts['REPLAY_API'] = '127.0.0.1' @connect_hosts['REPLAY_PREIDENTIFIED'] = '127.0.0.1' @connect_hosts['REPLAY_CMD_ROUTER'] = '127.0.0.1' @connect_hosts['DART_STREAM'] = '127.0.0.1' @connect_hosts['DART_DECOM'] = '127.0.0.1' @paths = {} @paths['LOGS'] = File.join(USERPATH, 'outputs', 'logs') @paths['TMP'] = File.join(USERPATH, 'outputs', 'tmp') @paths['SAVED_CONFIG'] = File.join(USERPATH, 'outputs', 'saved_config') @paths['TABLES'] = File.join(USERPATH, 'outputs', 'tables') @paths['HANDBOOKS'] = File.join(USERPATH, 'outputs', 'handbooks') @paths['PROCEDURES'] = [File.join(USERPATH, 'procedures')] @paths['SEQUENCES'] = File.join(USERPATH, 'outputs', 'sequences') @paths['DART_DATA'] = File.join(USERPATH, 'outputs', 'dart', 'data') @paths['DART_LOGS'] = File.join(USERPATH, 'outputs', 'dart', 'logs') unless filename system_arg = false ARGV.each do |arg| if system_arg filename = File.join(USERPATH, 'config', 'system', arg) break end system_arg = true if arg == '--system' end filename = File.join(USERPATH, 'config', 'system', 'system.txt') unless filename end process_file(filename) ENV['COSMOS_LOGS_DIR'] = @paths['LOGS'] @initial_filename = filename @initial_config = nil end # Reset variables and load packets def reset(filename = nil) reset_variables(filename) load_packets() end # Class level convenience reset method def self.reset self.instance.reset end def find_configuration(name) Cosmos.set_working_dir do Dir.foreach(@paths['SAVED_CONFIG']) do |filename| full_path = File.join(@paths['SAVED_CONFIG'], filename) if File.exist?(full_path) && File.basename(filename, ".*")[-32..-1] == name return full_path end end end nil end protected def unzip(zip_file_name) zip_dir = File.join(@paths['TMP'], File.basename(zip_file_name, ".*")) # Only unzip if we have to. We assume the unzipped directory structure is # intact. If not they'll get a popop with the errors encountered when # loading the configuration. unless File.exist? zip_dir Zip::File.open(zip_file_name) do |zip_file| zip_file.each do |entry| path = File.join(@paths['TMP'], entry.name) FileUtils.mkdir_p(File.dirname(path)) zip_file.extract(entry, path) unless File.exist?(path) end end end zip_dir end # A helper method to make the zip writing recursion work def write_zip_entries(base_dir, entries, zip_path, io) io.add(zip_path, base_dir) # Add the directory whether it has entries or not entries.each do |e| zip_file_path = File.join(zip_path, e) disk_file_path = File.join(base_dir, e) if File.directory? disk_file_path recursively_deflate_directory(disk_file_path, io, zip_file_path) else put_into_archive(disk_file_path, io, zip_file_path) end end end def recursively_deflate_directory(disk_file_path, io, zip_file_path) io.add(zip_file_path, disk_file_path) entries = Dir.entries(disk_file_path) - %w(. ..) write_zip_entries(disk_file_path, entries, zip_file_path, io) end def put_into_archive(disk_file_path, io, zip_file_path) io.get_output_stream(zip_file_path) do |f| data = nil File.open(disk_file_path, 'rb') do |file| data = file.read end f.write(data) end end def auto_detect_gem_based_targets Bundler.load.specs.each do |spec| spec_name_split = spec.name.split('-') if spec_name_split.length > 1 && (spec_name_split[0] == 'cosmos') # Filter to just targets and not tools and other extensions if File.exist?(File.join(spec.gem_dir, 'cmd_tlm')) target_name = spec_name_split[1..-1].join('-').to_s.upcase target = Target.new(target_name, nil, nil, nil, spec.gem_dir) @targets[target.name] = target end end end rescue Bundler::GemfileNotFound # No Gemfile - so no gem based targets end def update_config(config) current_config = @config unless @config @config = config @commands = Commands.new(config) @telemetry = Telemetry.new(config) @limits = Limits.new(config) else @config = config @commands.config = config @telemetry.config = config @limits.config = config end @telemetry.reset if current_config != config end def save_configuration Cosmos.set_working_dir do configuration = find_configuration(@config.name) configuration = File.join(@paths['SAVED_CONFIG'], File.build_timestamped_filename([@config.name], '.zip')) unless configuration unless File.exist?(configuration) begin Zip.continue_on_exists_proc = true Zip::File.open(configuration, Zip::File::CREATE) do |zipfile| zip_file_path = File.basename(configuration, ".zip") zipfile.mkdir zip_file_path # Copy target files into archive zip_targets = [] @targets.each do |target_name, target| entries = Dir.entries(target.dir) - %w(. ..) zip_target = File.join(zip_file_path, target.original_name) # Check the stored list of targets. We can't ask the zip file # itself because it's in progress and hasn't been saved unless zip_targets.include?(zip_target) write_zip_entries(target.dir, entries, zip_target, zipfile) zip_targets << zip_target end end # Create custom system.txt file zipfile.get_output_stream(File.join(zip_file_path, 'system.txt')) do |file| @targets.each do |target_name, target| target_filename = File.basename(target.filename) target_filename = nil unless File.exist?(target.filename) # Create a newline character since Zip opens files in binary mode newline = Kernel.is_windows? ? "\r\n" : "\n" if target.substitute file.write "DECLARE_TARGET #{target.original_name} #{target.name} #{target_filename}#{newline}" else file.write "DECLARE_TARGET #{target.name} nil #{target_filename}#{newline}" end end end end File.chmod(0444, configuration) # Mark readonly rescue Exception => error Logger.error "Problem saving configuration to #{configuration}: #{error.class}:#{error.message}\n#{error.backtrace.join("\n")}\n" end end end end def load_packets(configuration_name = nil) # Determine MD5 over all targets cmd_tlm files cmd_tlm_files = [] additional_data = '' @targets.each do |target_name, target| cmd_tlm_files << target.filename if File.exist?(target.filename) cmd_tlm_files.concat(target.cmd_tlm_files) if target.substitute additional_data << target.original_name additional_data << target.name else additional_data << target.original_name end end md5 = Cosmos.md5_files(cmd_tlm_files + @additional_md5_files, additional_data) md5_string = md5.hexdigest # Build filename for marshal file marshal_filename = File.join(@paths['TMP'], 'marshal_' << md5_string << '.bin') # Attempt to load marshal file config = Cosmos.marshal_load(marshal_filename) if config update_config(config) @config.name = configuration_name if configuration_name # Marshal file load successful Logger.info "Marshal load success: #{marshal_filename}" @config.warnings.each {|warning| Logger.warn(warning)} if @config.warnings else # Marshal file load failed - Manually load configuration @config = PacketConfig.new @commands = Commands.new(@config) @telemetry = Telemetry.new(@config) @limits = Limits.new(@config) @targets.each do |target_name, target| target.cmd_tlm_files.each do |cmd_tlm_file| begin @config.process_file(cmd_tlm_file, target.name) rescue Exception => err Logger.error "Problem processing #{cmd_tlm_file}." raise err end end end # Create marshal file for next time if configuration_name @config.name = configuration_name else @config.name = md5_string end Cosmos.marshal_dump(marshal_filename, @config) end setup_system_meta() @initial_config = @config unless @initial_config save_configuration() end def setup_system_meta # Ensure SYSTEM META is defined and defined correctly begin if @commands.target_names.include?("SYSTEM") pkts = @commands.packets('SYSTEM') # User should not define COMMAND SYSTEM META as we build it to match TELEMETRY raise "COMMAND SYSTEM META defined" if pkts.keys.include?('META') end tlm_meta = @telemetry.packet('SYSTEM', 'META') item = tlm_meta.get_item('PKTID') raise "PKTID incorrect" unless (item.bit_size == 8) && (item.bit_offset == 0) item = tlm_meta.get_item('CONFIG') raise "CONFIG incorrect" unless (item.bit_size == 256) && (item.bit_offset == 8) item = tlm_meta.get_item('COSMOS_VERSION') raise "COSMOS_VERSION incorrect" unless (item.bit_size == 240) && (item.bit_offset == 264) item = tlm_meta.get_item('USER_VERSION') raise "USER_VERSION incorrect" unless (item.bit_size == 240) && (item.bit_offset == 504) item = tlm_meta.get_item('RUBY_VERSION') raise "RUBY_VERSION incorrect" unless (item.bit_size == 240) && (item.bit_offset == 744) cmd_meta = build_cmd_system_meta() rescue => err Logger.error "SYSTEM META not defined correctly due to #{err.message} - defaulting" tlm_meta = build_tlm_system_meta() cmd_meta = build_cmd_system_meta() end # Initialize the meta packet (if given init filename) if @meta_init_filename parser = ConfigParser.new("http://cosmosrb.com/docs/cmdtlm") Cosmos.set_working_dir do parser.parse_file(@meta_init_filename) do |keyword, params| begin item = tlm_meta.get_item(keyword) if (item.data_type == :STRING) || (item.data_type == :BLOCK) value = params[0] else value = params[0].convert_to_value end tlm_meta.write(keyword, value) rescue => err raise parser.error(err, "ITEM_NAME VALUE") end end end end # Setup fixed part of SYSTEM META packet tlm_meta.write('PKTID', 1) tlm_meta.write('CONFIG', @config.name) tlm_meta.write('COSMOS_VERSION', "#{COSMOS_VERSION}") tlm_meta.write('USER_VERSION', USER_VERSION) if defined? USER_VERSION tlm_meta.write('RUBY_VERSION', "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}") cmd_meta.buffer = tlm_meta.buffer cmd_meta.received_time = Time.now.sys tlm_meta.received_time = Time.now.sys end def build_cmd_system_meta cmd_meta = Packet.new('SYSTEM', 'META', :BIG_ENDIAN) cmd_meta.disabled = true tlm_meta = @telemetry.packet('SYSTEM', 'META') tlm_meta.sorted_items.each do |item| next if item.name.include?("RECEIVED") # Tlm only items cmd_meta.define(item.clone) end @config.commands['SYSTEM'] ||= {} @config.commands['SYSTEM']['META'] = cmd_meta cmd_meta end def build_tlm_system_meta tlm_meta = Packet.new('SYSTEM', 'META', :BIG_ENDIAN) tlm_meta.define_reserved_items() item = tlm_meta.append_item('PKTID', 8, :UINT, nil, :BIG_ENDIAN, :ERROR, nil, nil, nil, 1) item.description = 'Packet Id' item.meta["READ_ONLY"] = [] item = tlm_meta.append_item('CONFIG', 32 * 8, :STRING) item.description = 'Configuration Name' item.meta["READ_ONLY"] = [] item = tlm_meta.append_item('COSMOS_VERSION', 30 * 8, :STRING) item.description = 'COSMOS Version' item.meta["READ_ONLY"] = [] item = tlm_meta.append_item('USER_VERSION', 30 * 8, :STRING) item.description = 'User Project Version' item.meta["READ_ONLY"] = [] item = tlm_meta.append_item('RUBY_VERSION', 30 * 8, :STRING) item.description = 'Ruby Version' item.meta["READ_ONLY"] = [] @config.telemetry['SYSTEM'] ||= {} @config.telemetry['SYSTEM']['META'] = tlm_meta tlm_meta end end end