lib/cosmos/tools/tlm_extractor/tlm_extractor.rb in cosmos-3.5.0 vs lib/cosmos/tools/tlm_extractor/tlm_extractor.rb in cosmos-3.5.1

- old
+ new

@@ -1,1016 +1,1016 @@ -# 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' -Cosmos.catch_fatal_exception do - require 'cosmos/tools/tlm_extractor/tlm_extractor_processor' - require 'cosmos/tools/tlm_extractor/text_item_chooser' - require 'cosmos/gui/qt_tool' - require 'cosmos/gui/choosers/telemetry_chooser' - require 'cosmos/gui/choosers/float_chooser' - require 'cosmos/gui/dialogs/splash' - require 'cosmos/gui/widgets/packet_log_frame' - require 'cosmos/gui/dialogs/progress_dialog' - require 'cosmos/gui/widgets/full_text_search_line_edit' -end - -module Cosmos - - # TlmExtractor class - # - # This class implements the TlmExtractor. This application breaks a binary log of telemetry - # into a csv type file - # - class TlmExtractor < QtTool - slots 'context_menu(const QPoint&)' - - FORMATTING_OPTIONS = %w(CONVERTED RAW FORMATTED WITH_UNITS) - - class MyListWidget < Qt::ListWidget - signals 'enterKeyPressed(int)' - - def keyPressEvent(event) - case event.key - when Qt::Key_Delete, Qt::Key_Backspace - remove_selected_items() - when Qt::Key_Return, Qt::Key_Enter - emit enterKeyPressed(currentRow()) - end - super(event) - end - - def remove_selected_items - indexes = selected_items() - indexes.reverse_each do |index| - item = takeItem(index) - item.dispose if item - end - end - - # Returns an array with the indexes of the selected items - def selected_items - selected = [] - index = 0 - self.each do |list_item| - selected << index if list_item.selected? - index += 1 - end - selected - end - end - - # Constructor - def initialize(options) - super(options) # MUST BE FIRST - All code before super is executed twice in RubyQt Based classes - Cosmos.load_cosmos_icon("tlm_extractor.png") - - # Define instance variables - @input_filenames = [] - @log_dir = System.paths['LOGS'] - @config_dir = File.join(Cosmos::USERPATH, 'config', 'tools', 'tlm_extractor', '') - @cancel = false - - initialize_actions() - initialize_menus() - initialize_central_widget() - complete_initialize() - - # Bring up slash screen for long duration tasks after creation - Splash.execute(self) do |splash| - # Configure CosmosConfig to interact with splash screen - ConfigParser.splash = splash - - System.telemetry - @search_box.completion_list = System.telemetry.all_item_strings(true, splash) - @tlm_extractor_config = TlmExtractorConfig.new(options.config_file) - @tlm_extractor_processor = TlmExtractorProcessor.new - Qt.execute_in_main_thread(true) do - @telemetry_chooser.update - sync_config_to_gui() - end - - # Unconfigure CosmosConfig to interact with splash screen - ConfigParser.splash = nil - end - end - - def initialize_actions - super() - - # File Menu Actions - @open_config = Qt::Action.new(tr('Open &Config'), self) - @open_config.statusTip = tr('Open configuration file') - @open_config.connect(SIGNAL('triggered()')) { handle_browse_button() } - - @save_config = Qt::Action.new(tr('&Save Config'), self) - @save_config_keyseq = Qt::KeySequence.new(tr('Ctrl+S')) - @save_config.shortcut = @save_config_keyseq - @save_config.statusTip = tr('Save current configuration') - @save_config.connect(SIGNAL('triggered()')) { handle_save_button() } - - @file_options = Qt::Action.new(tr('O&ptions'), self) - @file_options.statusTip = tr('Open the options dialog') - @file_options.connect(SIGNAL('triggered()')) { handle_options() } - - # Mode Menu Actions - @fill_down_check = Qt::Action.new(tr('&Fill Down'), self) - @fill_down_check_keyseq = Qt::KeySequence.new(tr('Ctrl+F')) - @fill_down_check.shortcut = @fill_down_check_keyseq - @fill_down_check.statusTip = tr('Fill Down') - @fill_down_check.setCheckable(true) - - @matlab_header_check = Qt::Action.new(tr('&Matlab Header'), self) - @matlab_header_check_keyseq = Qt::KeySequence.new(tr('Ctrl+M')) - @matlab_header_check.shortcut = @matlab_header_check_keyseq - @matlab_header_check.statusTip = tr('Add a Matlab header to the output data') - @matlab_header_check.setCheckable(true) - - @share_columns_check = Qt::Action.new(tr('&Share Columns'), self) - @share_columns_check.statusTip = tr('Share columns for items with the same name') - @share_columns_check.setCheckable(true) - - @unique_only_check = Qt::Action.new(tr('&Unique Only'), self) - @unique_only_check_keyseq = Qt::KeySequence.new(tr('Ctrl+U')) - @unique_only_check.shortcut = @unique_only_check_keyseq - @unique_only_check.statusTip = tr('Only output rows where data has changed') - @unique_only_check.setCheckable(true) - - @batch_mode_check = Qt::Action.new(tr('&Batch Mode'), self) - @batch_mode_check_keyseq = Qt::KeySequence.new(tr('Ctrl+B')) - @batch_mode_check.shortcut = @batch_mode_check_keyseq - @batch_mode_check.statusTip = tr('Process multiple config files with the same input files') - @batch_mode_check.setCheckable(true) - @batch_mode_check.connect(SIGNAL('triggered()')) { batch_mode_changed() } - - # Item Menu Actions - @item_edit = Qt::Action.new(tr('&Edit Items'), self) - @item_edit_keyseq = Qt::KeySequence.new(tr('Ctrl+E')) - @item_edit.shortcut = @item_edit_keyseq - @item_edit.statusTip = tr('Options') - @item_edit.connect(SIGNAL('triggered()')) { item_edit() } - - @item_delete = Qt::Action.new(tr('&Delete Items'), self) - @item_delete.statusTip = tr('Options') - @item_delete.connect(SIGNAL('triggered()')) { item_delete() } - end - - def initialize_menus - # File Menu - @file_menu = menuBar.addMenu(tr('&File')) - @file_menu.addAction(@open_config) - @file_menu.addAction(@save_config) - @file_menu.addSeparator() - @file_menu.addAction(@file_options) - @file_menu.addSeparator() - @file_menu.addAction(@exit_action) - - # Mode Menu - @mode_menu = menuBar.addMenu(tr('&Mode')) - @mode_menu.addAction(@fill_down_check) - @mode_menu.addAction(@matlab_header_check) - @mode_menu.addAction(@share_columns_check) - @mode_menu.addAction(@unique_only_check) - @mode_menu.addAction(@batch_mode_check) - - # Item Menu - @item_menu = menuBar.addMenu(tr('&Item')) - @item_menu.addAction(@item_edit) - @item_menu.addAction(@item_delete) - - # Help Menu - @about_string = "COSMOS Telemetry Extractor allows processing of telemetry log files and breaking out specified items." - initialize_help_menu() - end - - def initialize_central_widget - # Create the central widget - @central_widget = Qt::Widget.new - setCentralWidget(@central_widget) - - @top_layout = Qt::VBoxLayout.new - - @config_box = Qt::GroupBox.new("Configuration") - @config_box_layout = Qt::VBoxLayout.new - @config_box.setLayout(@config_box_layout) - @top_layout.addWidget(@config_box) - - # Configuration File Selector - @config_layout = Qt::HBoxLayout.new - @config_layout_label = Qt::Label.new('Configuration File:') - @config_layout.addWidget(@config_layout_label) - @config_field = Qt::LineEdit.new - @config_field.setReadOnly(true) - @config_layout.addWidget(@config_field) - @save_button = Qt::PushButton.new('Save...') - @config_layout.addWidget(@save_button) - @save_button.connect(SIGNAL('clicked()')) { handle_save_button() } - @browse_button = Qt::PushButton.new('Browse...') - @config_layout.addWidget(@browse_button) - @browse_button.connect(SIGNAL('clicked()')) { handle_browse_button() } - @config_box_layout.addLayout(@config_layout) - - # Separator before editor - @sep1 = Qt::Frame.new(@central_widget) - @sep1.setFrameStyle(Qt::Frame::HLine | Qt::Frame::Sunken) - @config_box_layout.addWidget(@sep1) - - # Configuration File Editor - @config_item_list = MyListWidget.new(self) - @config_item_list.setContextMenuPolicy(Qt::CustomContextMenu) - @config_item_list.setDragDropMode(Qt::AbstractItemView::InternalMove) - @config_item_list.setSelectionMode(Qt::AbstractItemView::ExtendedSelection) - @config_item_list.setMinimumHeight(150) - # Allow either double click or enter / return key to bring up the item editor - @config_item_list.connect(SIGNAL('itemDoubleClicked(QListWidgetItem*)')) { item_list_editor() } - @config_item_list.connect(SIGNAL('enterKeyPressed(int)')) { item_list_editor() } - connect(@config_item_list, SIGNAL('customContextMenuRequested(const QPoint&)'), self, SLOT('context_menu(const QPoint&)')) - @config_box_layout.addWidget(@config_item_list) - - # Telemetry Search - @search_layout = Qt::HBoxLayout.new - @search_box = FullTextSearchLineEdit.new(self) - @search_box.setStyleSheet("padding-right: 20px;padding-left: 5px;background: url(#{File.join(Cosmos::PATH, 'data', 'search-14.png')});background-position: right;background-repeat: no-repeat;") - @search_add_item_button = Qt::PushButton.new('Add Item') - @search_add_item_button.connect(SIGNAL('clicked()')) do - split_tlm = @search_box.text.to_s.split(" ") - if split_tlm.length == 3 - target_name = split_tlm[0].to_s.upcase - packet_name = split_tlm[1].to_s.upcase - item_name = split_tlm[2].to_s.upcase - begin - System.telemetry.packet_and_item(target_name, packet_name, item_name) - add_item_callback(target_name, packet_name, item_name) - rescue - # Does not exist - end - end - end - @search_add_packet_button = Qt::PushButton.new('Add Packet') - @search_add_packet_button.connect(SIGNAL('clicked()')) do - split_tlm = @search_box.text.to_s.split(" ") - if split_tlm.length >= 2 - target_name = split_tlm[0].to_s.upcase - packet_name = split_tlm[1].to_s.upcase - begin - System.telemetry.packet(target_name, packet_name) - add_packet_callback(target_name, packet_name) - rescue - # Does not exist - end - end - end - @search_add_target_button = Qt::PushButton.new('Add Target') - @search_add_target_button.connect(SIGNAL('clicked()')) do - split_tlm = @search_box.text.to_s.split(" ") - if split_tlm.length >= 1 - target_name = split_tlm[0].to_s.upcase - if System.telemetry.target_names.include?(target_name) - add_target_callback(target_name) - end - end - end - @search_layout.addWidget(@search_box) - @search_layout.addWidget(@search_add_item_button) - @search_layout.addWidget(@search_add_packet_button) - @search_layout.addWidget(@search_add_target_button) - @config_box_layout.addLayout(@search_layout) - - # Telemetry Chooser - tlm_chooser_layout = Qt::BoxLayout.new(Qt::Horizontal) - add_target_button = Qt::PushButton.new('Add Target') - add_target_button.connect(SIGNAL('clicked()')) do - add_target_callback(@telemetry_chooser.target_name) - end - tlm_chooser_layout.addWidget(add_target_button) - add_packet_button = Qt::PushButton.new('Add Packet') - add_packet_button.connect(SIGNAL('clicked()')) do - add_packet_callback(@telemetry_chooser.target_name, @telemetry_chooser.packet_name) - end - tlm_chooser_layout.addWidget(add_packet_button) - @telemetry_chooser = TelemetryChooser.new(self, Qt::Horizontal, true, true, false, true) - @telemetry_chooser.button_text = 'Add Item' - @telemetry_chooser.select_button_callback = method(:add_item_callback) - tlm_chooser_layout.addWidget(@telemetry_chooser) - @config_box_layout.addLayout(tlm_chooser_layout) - - # Text Item Chooser - @text_item_chooser = TextItemChooser.new(self) - @text_item_chooser.button_callback = method(:add_text_item_callback) - @config_box_layout.addWidget(@text_item_chooser) - - # Downsample - @downsample_entry = FloatChooser.new(self, 'Downsample Seconds:', 0.0, 0.0, nil, 20, true) - @config_box_layout.addWidget(@downsample_entry) - - # Batch Configuration - @batch_config_box = Qt::GroupBox.new("Batch Configuration") - @batch_config_layout = Qt::GridLayout.new - @batch_config_layout.setColumnStretch(1, 1) - @batch_config_box.setLayout(@batch_config_layout) - @top_layout.addWidget(@batch_config_box) - - row = 0 - - # Chooser for Log Files - label = Qt::Label.new('Config Files:') - @batch_config_layout.addWidget(label, row, 0) - @batch_fill_widget = Qt::Widget.new - @batch_fill_widget.setMinimumWidth(360) - @batch_config_layout.addWidget(@batch_fill_widget, row, 1, 0, 3) - @batch_browse_button = Qt::PushButton.new('Browse...') - @batch_browse_button.connect(SIGNAL('clicked()')) { handle_batch_browse_button() } - @batch_config_layout.addWidget(@batch_browse_button, row, 2) - @batch_remove_button = Qt::PushButton.new('Remove') - @batch_remove_button.connect(SIGNAL('clicked()')) { handle_batch_remove_button() } - @batch_config_layout.addWidget(@batch_remove_button, row, 3) - row += 1 - - @batch_filenames_entry = Qt::ListWidget.new(self) - @batch_filenames_entry.setSelectionMode(Qt::AbstractItemView::ExtendedSelection) - @batch_filenames_entry.setSortingEnabled(true) - @batch_filenames_entry.setMinimumHeight(90) - @batch_config_layout.addWidget(@batch_filenames_entry, row, 0, 3, 4) - row += 3 - - # Batch Name - @batch_name_label = Qt::Label.new('Batch Name:') - @batch_config_layout.addWidget(@batch_name_label, row, 0) - @batch_name_entry = Qt::LineEdit.new - @batch_name_entry.setMinimumWidth(340) - @batch_config_layout.addWidget(@batch_name_entry, row, 1, 1, 3) - row += 1 - - @batch_config_box.hide - - @file_box = Qt::GroupBox.new("File Selection") - @file_box_layout = Qt::VBoxLayout.new - @file_box.setLayout(@file_box_layout) - @top_layout.addWidget(@file_box) - - # Packet Log Frame - @packet_log_frame = PacketLogFrame.new(self, @log_dir, System.default_packet_log_reader.new, @input_filenames, nil, true, true, true, Cosmos::TLM_FILE_PATTERN, Cosmos::TXT_FILE_PATTERN) - @packet_log_frame.change_callback = method(:change_callback) - @file_box_layout.addWidget(@packet_log_frame) - - # Process and Open Buttons - @button_layout = Qt::HBoxLayout.new - - @process_button = Qt::PushButton.new('&Process Files') - @process_button.connect(SIGNAL('clicked()')) { process_log_files() } - @button_layout.addWidget(@process_button) - - @open_button = Qt::PushButton.new('&Open in Text Editor') - @open_button.connect(SIGNAL('clicked()')) { open_button() } - @open_button.setEnabled(false) - @button_layout.addWidget(@open_button) - - if Kernel.is_windows? - @open_excel_button = Qt::PushButton.new('&Open in Excel') - @open_excel_button.connect(SIGNAL('clicked()')) { open_excel_button() } - @open_excel_button.setEnabled(false) - @button_layout.addWidget(@open_excel_button) - end - @top_layout.addLayout(@button_layout) - - @central_widget.setLayout(@top_layout) - end # def initialize - - def self.post_options_parsed_hook(options) - if options.input_files - # Process config file - raise "Configuration File must be specified for command line processing" unless options.config_file - - # Process the input file(s) - tlm_extractor_config = TlmExtractorConfig.new(options.config_file) - tlm_extractor_processor = TlmExtractorProcessor.new - unless options.output_file - filename = options.input_files[0] - extension = File.extname(filename) - filename_no_extension = filename[0..-(extension.length + 1)] - if tlm_extractor_config.delimiter.to_s.strip == ',' - filename = filename_no_extension << '.csv' - else - filename = filename_no_extension << '.txt' - end - options.output_file = File.join(System.paths['LOGS'], File.basename(filename)) - end - - tlm_extractor_config.output_filename = options.output_file - tlm_extractor_processor.process(options.input_files, [tlm_extractor_config]) - puts "Created #{options.output_file}" - return false - else - return true - end - end - - # Runs the application - def self.run(option_parser = nil, options = nil) - Cosmos.catch_fatal_exception do - unless option_parser and options - option_parser, options = create_default_options() - options.width = 700 - options.height = 425 - options.auto_size = false - options.restore_size = false # always render this the correct size - options.title = "Telemetry Extractor" - option_parser.separator "Telemetry Extractor Specific Options:" - option_parser.on("-c", "--config FILE", "Use the specified configuration file") do |arg| - options.config_file = File.join(Cosmos::USERPATH, 'config', 'tools', 'tlm_extractor', arg) - end - option_parser.on("-i", "--input FILE", "Process the specified input file") do |arg| - options.input_files ||= [] - if arg[0..0] != '/' and arg[1..1] != ':' - # Relative path to default of log folder - arg = File.join(System.paths['LOGS'], arg) - end - options.input_files << arg - end - option_parser.on("-o", "--output FILE", "Output results to the specified file") do |arg| - options.output_file = arg - end - end - - super(option_parser, options) - end - end # def self.run - - def context_menu(point) - @item_menu.exec(@config_item_list.mapToGlobal(point)) - end - - protected - - def sync_gui_to_config - @tlm_extractor_config.matlab_header = @matlab_header_check.checked? - @tlm_extractor_config.fill_down = @fill_down_check.checked? - @tlm_extractor_config.share_columns = @share_columns_check.checked? - @tlm_extractor_config.unique_only = @unique_only_check.checked? - @tlm_extractor_config.downsample_seconds = @downsample_entry.value - @tlm_extractor_config.output_filename = @packet_log_frame.output_filename - - @tlm_extractor_config.clear_items - @config_item_list.each do |item| - split_item = item.text.scan ConfigParser::PARSING_REGEX - item_type = split_item[0] - target_name_or_column_name = split_item[1] - packet_name_or_text = split_item[2] - item_name = split_item[3] - value_type = split_item[4] - if value_type - value_type = value_type.upcase.intern - else - value_type = :CONVERTED - end - if item_type == 'ITEM' - @tlm_extractor_config.add_item(target_name_or_column_name, packet_name_or_text, item_name, value_type) - else - @tlm_extractor_config.add_text(target_name_or_column_name.remove_quotes, packet_name_or_text.remove_quotes) - end - end - end - - def sync_config_to_gui - @matlab_header_check.setChecked(@tlm_extractor_config.matlab_header) - @fill_down_check.setChecked(@tlm_extractor_config.fill_down) - @share_columns_check.setChecked(@tlm_extractor_config.share_columns) - @unique_only_check.setChecked(@tlm_extractor_config.unique_only) - @downsample_entry.value = @tlm_extractor_config.downsample_seconds - - clear_config_item_list() - @tlm_extractor_config.items.each do |item_type, target_name_or_column_name, packet_name_or_text, item_name, value_type| - if item_type == 'ITEM' - if value_type == :CONVERTED - @config_item_list.addItem("#{item_type} #{target_name_or_column_name} #{packet_name_or_text} #{item_name}") - else - @config_item_list.addItem("#{item_type} #{target_name_or_column_name} #{packet_name_or_text} #{item_name} #{value_type}") - end - else - @config_item_list.addItem("#{item_type} \"#{target_name_or_column_name}\" \"#{packet_name_or_text}\"") - end - end - end - - ############################################################################### - # File Menu Handlers - ############################################################################### - - # Handles processing log files - def process_log_files - @cancel = false - begin - @tlm_extractor_processor.packet_log_reader = @packet_log_frame.packet_log_reader - @input_filenames = @packet_log_frame.filenames.sort - @batch_filenames = [] - output_extension = '.txt' - batch_name = nil - if @batch_mode_check.checked? - batch_name = @batch_name_entry.text - @batch_filenames_entry.each {|list_item| @batch_filenames << list_item.text} - if @packet_log_frame.output_filename_filter == Cosmos::CSV_FILE_PATTERN - output_extension = '.csv' - else - output_extension = '.txt' - end - end - return unless pre_process_tests() - - # Configure Tlm Extractor Config - sync_gui_to_config() - - start_time = Time.now - ProgressDialog.execute(self, 'Log File Progress', 600, 300) do |progress_dialog| - progress_dialog.cancel_callback = method(:cancel_callback) - progress_dialog.enable_cancel_button - - begin - current_input_file_index = -1 - current_config_file_index = -1 - start_packet_count = -1 - last_packet_count = -1 - - if @batch_filenames.empty? - process_method = :process - process_args = [@input_filenames, [@tlm_extractor_config], @packet_log_frame.time_start, @packet_log_frame.time_end] - else - process_method = :process_batch - process_args = [batch_name, @input_filenames, @log_dir, output_extension, @batch_filenames, @packet_log_frame.time_start, @packet_log_frame.time_end] - end - - @tlm_extractor_processor.send(process_method, *process_args) do |input_file_index, packet_count, file_progress| - # Handle Cancel - break if @cancel - - # Handle Input File Change - if input_file_index != current_input_file_index - current_input_file_index = input_file_index - - if start_packet_count >= 0 - # Make sure some packets were found in the previous file - if packet_count == start_packet_count - # No packets found in previous file - progress_dialog.append_text(" WARNING: No packets processed in #{File.basename(@input_filenames[input_file_index - 1])}") - end - end - start_packet_count = packet_count - - progress_dialog.append_text("Processing File #{input_file_index + 1}/#{@input_filenames.length}: #{File.basename(@input_filenames[input_file_index])}") - progress_dialog.set_step_progress(0.0) - progress_dialog.set_overall_progress((input_file_index).to_f / @input_filenames.length.to_f) - end - - # Save packet_count - last_packet_count = packet_count - - # Handle Progress Reporting - progress_dialog.set_step_progress(file_progress) - end - # Make sure some packets were found in the previous file - if start_packet_count == last_packet_count - # No packets found in previous file - progress_dialog.append_text(" WARNING: No packets processed in #{File.basename(@input_filenames[-1])}") - end - - rescue => error - progress_dialog.append_text("Error processing:\n#{error.formatted}\n") - ensure - progress_dialog.set_step_progress(1.0) if !@cancel - progress_dialog.set_overall_progress(1.0) if !@cancel - progress_dialog.append_text("Runtime: #{Time.now - start_time} s") - progress_dialog.complete - if @batch_filenames.empty? - Qt.execute_in_main_thread(true) do - @open_button.setEnabled(true) - @open_excel_button.setEnabled(true) if Kernel.is_windows? - end - end - end - end # ProgressDialog.execute - rescue => error - Qt::MessageBox.critical(self, 'Error!', "Error Processing Log File(s)\n#{error.formatted}") - end - end # def process_log_files - - # Handles options dialog - def handle_options - box = Qt::Dialog.new(self) - box.setWindowTitle('Options') - top_layout = Qt::VBoxLayout.new - - delimiter_layout = Qt::HBoxLayout.new - delimiter_label = Qt::Label.new('Delimeter:') - delimiter_layout.addWidget(delimiter_label) - delimiter_field = Qt::LineEdit.new - delimiter_layout.addWidget(delimiter_field) - if @tlm_extractor_config.delimiter != "\t" - delimiter_field.setText(@tlm_extractor_config.delimiter) - else - delimiter_field.setText('tab') - end - top_layout.addLayout(delimiter_layout) - - checkbox_layout = Qt::HBoxLayout.new - checkbox_label = Qt::Label.new('Output filenames') - checkbox_layout.addWidget(checkbox_label) - checkbox_field = Qt::CheckBox.new - if @tlm_extractor_config.print_filenames_to_output - checkbox_field.setChecked(true) - else - checkbox_field.setChecked(false) - end - checkbox_layout.addWidget(checkbox_field) - top_layout.addLayout(checkbox_layout) - - button_layout = Qt::HBoxLayout.new - ok_button = Qt::PushButton.new('OK') - ok_button.connect(SIGNAL('clicked()')) { box.accept } - button_layout.addWidget(ok_button) - button_layout.addStretch - cancel_button = Qt::PushButton.new('CANCEL') - cancel_button.connect(SIGNAL('clicked()')) { box.reject } - button_layout.addWidget(cancel_button) - top_layout.addLayout(button_layout) - - box.setLayout(top_layout) - case box.exec - when Qt::Dialog::Accepted - delimiter = delimiter_field.text - if delimiter == 'tab' - delimiter = "\t" - end - @tlm_extractor_config.print_filenames_to_output = checkbox_field.checked? - @tlm_extractor_config.delimiter = delimiter - if @tlm_extractor_config.delimiter.to_s.strip == ',' - @packet_log_frame.output_filename_filter = Cosmos::CSV_FILE_PATTERN - else - @packet_log_frame.output_filename_filter = Cosmos::TXT_FILE_PATTERN - end - end - box.dispose - end - - def batch_mode_changed - if @batch_mode_check.checked? - @config_box.hide - @batch_config_box.show - @open_button.setEnabled(false) - @open_excel_button.setEnabled(false) if Kernel.is_windows? - @fill_down_check.setEnabled(false) - @matlab_header_check.setEnabled(false) - @share_columns_check.setEnabled(false) - @unique_only_check.setEnabled(false) - @open_config.setEnabled(false) - @save_config.setEnabled(false) - @file_options.setEnabled(false) - @item_edit.setEnabled(false) - @item_delete.setEnabled(false) - @packet_log_frame.select_output_dir - @packet_log_frame.output_filename = @log_dir - else - @config_box.show - @batch_config_box.hide - @fill_down_check.setEnabled(true) - @matlab_header_check.setEnabled(true) - @share_columns_check.setEnabled(true) - @unique_only_check.setEnabled(true) - @open_config.setEnabled(true) - @save_config.setEnabled(true) - @file_options.setEnabled(true) - @item_edit.setEnabled(true) - @item_delete.setEnabled(true) - @packet_log_frame.select_output_file - @packet_log_frame.output_filename = '' - end - end - - ############################################################################### - # Item Menu Handlers - ############################################################################### - - def item_edit - item_list_editor() - end - - def item_delete - @config_item_list.remove_selected_items - end - - ############################################################################### - # Handlers - ############################################################################### - - def handle_save_button - filename = nil - begin - if @config_field.text.strip.length > 0 - filename = Qt::FileDialog.getSaveFileName(self, "Save Config File", @config_field.text, "Config Files (*.txt);;All Files (*)") - else - filename = Qt::FileDialog.getSaveFileName(self, "Save Config File", @config_dir, "Config Files (*.txt);;All Files (*)") - end - if filename and filename.length != 0 - sync_gui_to_config() - @tlm_extractor_config.save(filename) - @config_field.setText(filename) - @config_dir = File.dirname(filename) + '/' - end - rescue => error - Qt::MessageBox.critical(self, 'Error!', "Error Saving Configuration File: #{filename}\n#{error.formatted}") - end - end - - def handle_browse_button - filename = Qt::FileDialog::getOpenFileName(self, "Open Config File:", @config_dir, "Config Files (*.txt);;All Files (*)") - if filename and not filename.empty? - @config_field.setText(filename) - @config_dir = File.dirname(filename) + '/' - - begin - process_config_file(filename) - rescue => error - Qt::MessageBox.critical(self, 'Error', "Error Processing Configuration File: #{filename}\n\n#{error.formatted}") - end - end - end - - def item_list_editor - done_items = false - selected_items = @config_item_list.selected_items() - if @config_item_list.currentItem and !selected_items.empty? - selected_items.each do |item_index| - dialog = Qt::Dialog.new(self) - dialog.setWindowTitle("Edit Item") - layout = Qt::VBoxLayout.new - split_item = @config_item_list.item(item_index).text.scan ConfigParser::PARSING_REGEX - - if split_item[0] == 'ITEM' and !done_items - label = Qt::Label.new("#{split_item[1]} #{split_item[2]} #{split_item[3]}") - layout.addWidget(label) - - label = Qt::Label.new('Value Type:') - layout.addWidget(label) - - box = Qt::ComboBox.new - box.maxCount = FORMATTING_OPTIONS.length - FORMATTING_OPTIONS.each {|item| box.addItem(item) } - current_formatting = split_item[4] - current_formatting = 'CONVERTED' unless current_formatting - if FORMATTING_OPTIONS.index(current_formatting) - box.currentIndex = FORMATTING_OPTIONS.index(current_formatting) - end - layout.addWidget(box) - - check_box = nil - if selected_items.length > 1 and item_index == selected_items[0] - check_box = Qt::CheckBox.new('Apply to All?') - layout.addWidget(check_box) - end - - button_layout = Qt::BoxLayout.new(Qt::Horizontal) - ok = Qt::PushButton.new("Save") - connect(ok, SIGNAL('clicked()'), dialog, SLOT('accept()')) - button_layout.addWidget(ok) - cancel = Qt::PushButton.new("Cancel") - connect(cancel, SIGNAL('clicked()'), dialog, SLOT('reject()')) - button_layout.addWidget(cancel) - layout.addLayout(button_layout) - - dialog.setLayout(layout) - if dialog.exec == Qt::Dialog::Accepted - if box.currentIndex != -1 - set_indexes = [item_index] - if check_box and check_box.checked? - set_indexes = selected_items - end - - set_indexes.each do |set_item_index| - split_item = @config_item_list.item(set_item_index).text.scan ConfigParser::PARSING_REGEX - if split_item[0] == 'ITEM' - # Remove any formatting from the item by only keeping the first four strings - @config_item_list.item(set_item_index).text = split_item[0..3].join(' ') - - if box.currentIndex != 0 - @config_item_list.item(set_item_index).text = "#{@config_item_list.item(set_item_index).text} #{FORMATTING_OPTIONS[box.currentIndex]}" - end - end - end - if set_indexes.length > 1 - done_items = true - end - end - end - elsif split_item[0] == 'TEXT' - label = Qt::Label.new('Column Name:') - layout.addWidget(label) - column_name = Qt::LineEdit.new(split_item[1].remove_quotes) - layout.addWidget(column_name) - label = Qt::Label.new('Text:') - layout.addWidget(label) - text = Qt::LineEdit.new(split_item[2].remove_quotes) - layout.addWidget(text) - - button_layout = Qt::BoxLayout.new(Qt::Horizontal) - ok = Qt::PushButton.new("Save") - connect(ok, SIGNAL('clicked()'), dialog, SLOT('accept()')) - button_layout.addWidget(ok) - cancel = Qt::PushButton.new("Cancel") - connect(cancel, SIGNAL('clicked()'), dialog, SLOT('reject()')) - button_layout.addWidget(cancel) - layout.addLayout(button_layout) - - dialog.setLayout(layout) - if dialog.exec == Qt::Dialog::Accepted - # Remove any formatting from the item by only keeping the first four strings - @config_item_list.item(item_index).text = "#{split_item[0]} \"#{column_name.text}\" \"#{text.text}\"" - end - end - - dialog.dispose - end - end - end - - def open_button - Cosmos.open_in_text_editor(@packet_log_frame.output_filename) - end - - def open_excel_button - system("start Excel.exe \"#{@packet_log_frame.output_filename}\"") - end - - def clear_config_item_list - @config_item_list.clearItems - end # def clear_data_object_list - - # Handles removing a selected filename - def handle_batch_remove_button - @batch_filenames_entry.remove_selected_items - end - - # Handles browsing for log files - def handle_batch_browse_button - Cosmos.set_working_dir do - filenames = Qt::FileDialog::getOpenFileNames( - self, "Select Config File(s):", @config_dir, Cosmos::TXT_FILE_PATTERN) - if filenames and not filenames.empty? - @config_dir = File.dirname(filenames[0]) + '/' - filenames.each {|filename| @batch_filenames_entry.addItem(filename) if @batch_filenames_entry.findItems(filename, Qt::MatchExactly).empty? } - end - end - end - - ############################################################################### - # Additional Callbacks - ############################################################################### - - def cancel_callback(progress_dialog = nil) - @cancel = true - return true, false - end - - def add_target_callback(target_name) - packets = System.telemetry.packets(target_name) - packets.each do |packet_name, packet| - packet.sorted_items.each do |item| - @config_item_list.addItem("ITEM #{target_name} #{packet_name} #{item.name}") - end - end - end - - def add_packet_callback(target_name, packet_name) - packet = System.telemetry.packet(target_name, packet_name) - packet.sorted_items.each do |item| - @config_item_list.addItem("ITEM #{target_name} #{packet_name} #{item.name}") - end - end - - def add_item_callback(target_name, packet_name, item_name) - @config_item_list.addItem("ITEM #{target_name} #{packet_name} #{item_name}") - end - - def add_text_item_callback(column_name, text) - @config_item_list.addItem("TEXT \"#{column_name}\" \"#{text}\"") - end - - ############################################################################### - # Helper Methods - ############################################################################### - - def pre_process_tests - if !@batch_mode_check.checked? - # Normal Mode - - if @config_item_list.count < 1 - Qt::MessageBox.critical(self, 'Error', 'Please select at least 1 item') - return false - end - - unless @packet_log_frame.output_filename - Qt::MessageBox.critical(self, 'Error', 'No Output File Selected') - return false - end - - if File.exist?(@packet_log_frame.output_filename) - result = Qt::MessageBox.warning(self, 'Warning!', 'Output File Already Exists. Overwrite?', Qt::MessageBox::Yes | Qt::MessageBox::No) - return false if result == Qt::MessageBox::No - end - else - # Batch Mode - - if !@batch_name_entry.text or @batch_name_entry.text.strip.empty? - Qt::MessageBox.critical(self, 'Error', 'Batch Name is Required') - return false - end - - unless @batch_filenames and @batch_filenames[0] - Qt::MessageBox.critical(self, 'Error', 'Please select at least 1 config file') - return false - end - end - - unless @input_filenames and @input_filenames[0] - Qt::MessageBox.critical(self, 'Error', 'Please select at least 1 input file') - return false - end - - # Validate configurations exist for input filenames - @input_filenames.each do |input_filename| - Cosmos.check_log_configuration(@tlm_extractor_processor.packet_log_reader, input_filename) - end - - #Validate config information - @tlm_extractor_processor.packet_log_reader.open(@input_filenames[0]) - @tlm_extractor_processor.packet_log_reader.close - - @config_item_list.each do |item| - split_item = item.text.split - item_type = split_item[0] - target_name = split_item[1] - packet_name = split_item[2] - item_name = split_item[3] - - if item_type == 'ITEM' - # Verify Packet - packet = nil - begin - packet = System.telemetry.packet(target_name, packet_name) - rescue - Qt::MessageBox.critical(self, 'Error!', "Unknown Packet #{target_name} #{packet_name} specified") - return false - end - - # Verify Item - begin - packet.get_item(item_name) - rescue - Qt::MessageBox.critical(self, 'Error!', "Item #{item_name} not present in packet") - return false - end - end - end - - true - end - - def process_config_file(filename) - @tlm_extractor_config.restore(filename) - sync_config_to_gui() - end - - def change_callback(item_changed) - if item_changed == :INPUT_FILES - if !@batch_mode_check.checked? - filename = @packet_log_frame.filenames[0] - if filename - extension = File.extname(filename) - filename_no_extension = filename[0..-(extension.length + 1)] - if @packet_log_frame.output_filename_filter == Cosmos::CSV_FILE_PATTERN - filename = filename_no_extension << '.csv' - else - filename = filename_no_extension << '.txt' - end - @packet_log_frame.output_filename = filename - end - end - elsif item_changed == :OUTPUT_FILE - output_filename = @packet_log_frame.output_filename - if output_filename and !output_filename.to_s.strip.empty? - @log_dir = File.dirname(output_filename) - end - elsif item_changed == :OUTPUT_DIR - output_filename = @packet_log_frame.output_filename - if output_filename and !output_filename.to_s.strip.empty? - @log_dir = output_filename - end - end - end - - end # class TlmExtractor - -end # module Cosmos +# 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' +Cosmos.catch_fatal_exception do + require 'cosmos/tools/tlm_extractor/tlm_extractor_processor' + require 'cosmos/tools/tlm_extractor/text_item_chooser' + require 'cosmos/gui/qt_tool' + require 'cosmos/gui/choosers/telemetry_chooser' + require 'cosmos/gui/choosers/float_chooser' + require 'cosmos/gui/dialogs/splash' + require 'cosmos/gui/widgets/packet_log_frame' + require 'cosmos/gui/dialogs/progress_dialog' + require 'cosmos/gui/widgets/full_text_search_line_edit' +end + +module Cosmos + + # TlmExtractor class + # + # This class implements the TlmExtractor. This application breaks a binary log of telemetry + # into a csv type file + # + class TlmExtractor < QtTool + slots 'context_menu(const QPoint&)' + + FORMATTING_OPTIONS = %w(CONVERTED RAW FORMATTED WITH_UNITS) + + class MyListWidget < Qt::ListWidget + signals 'enterKeyPressed(int)' + + def keyPressEvent(event) + case event.key + when Qt::Key_Delete, Qt::Key_Backspace + remove_selected_items() + when Qt::Key_Return, Qt::Key_Enter + emit enterKeyPressed(currentRow()) + end + super(event) + end + + def remove_selected_items + indexes = selected_items() + indexes.reverse_each do |index| + item = takeItem(index) + item.dispose if item + end + end + + # Returns an array with the indexes of the selected items + def selected_items + selected = [] + index = 0 + self.each do |list_item| + selected << index if list_item.selected? + index += 1 + end + selected + end + end + + # Constructor + def initialize(options) + super(options) # MUST BE FIRST - All code before super is executed twice in RubyQt Based classes + Cosmos.load_cosmos_icon("tlm_extractor.png") + + # Define instance variables + @input_filenames = [] + @log_dir = System.paths['LOGS'] + @config_dir = File.join(Cosmos::USERPATH, 'config', 'tools', 'tlm_extractor', '') + @cancel = false + + initialize_actions() + initialize_menus() + initialize_central_widget() + complete_initialize() + + # Bring up slash screen for long duration tasks after creation + Splash.execute(self) do |splash| + # Configure CosmosConfig to interact with splash screen + ConfigParser.splash = splash + + System.telemetry + @search_box.completion_list = System.telemetry.all_item_strings(true, splash) + @tlm_extractor_config = TlmExtractorConfig.new(options.config_file) + @tlm_extractor_processor = TlmExtractorProcessor.new + Qt.execute_in_main_thread(true) do + @telemetry_chooser.update + sync_config_to_gui() + end + + # Unconfigure CosmosConfig to interact with splash screen + ConfigParser.splash = nil + end + end + + def initialize_actions + super() + + # File Menu Actions + @open_config = Qt::Action.new(tr('Open &Config'), self) + @open_config.statusTip = tr('Open configuration file') + @open_config.connect(SIGNAL('triggered()')) { handle_browse_button() } + + @save_config = Qt::Action.new(tr('&Save Config'), self) + @save_config_keyseq = Qt::KeySequence.new(tr('Ctrl+S')) + @save_config.shortcut = @save_config_keyseq + @save_config.statusTip = tr('Save current configuration') + @save_config.connect(SIGNAL('triggered()')) { handle_save_button() } + + @file_options = Qt::Action.new(tr('O&ptions'), self) + @file_options.statusTip = tr('Open the options dialog') + @file_options.connect(SIGNAL('triggered()')) { handle_options() } + + # Mode Menu Actions + @fill_down_check = Qt::Action.new(tr('&Fill Down'), self) + @fill_down_check_keyseq = Qt::KeySequence.new(tr('Ctrl+F')) + @fill_down_check.shortcut = @fill_down_check_keyseq + @fill_down_check.statusTip = tr('Fill Down') + @fill_down_check.setCheckable(true) + + @matlab_header_check = Qt::Action.new(tr('&Matlab Header'), self) + @matlab_header_check_keyseq = Qt::KeySequence.new(tr('Ctrl+M')) + @matlab_header_check.shortcut = @matlab_header_check_keyseq + @matlab_header_check.statusTip = tr('Add a Matlab header to the output data') + @matlab_header_check.setCheckable(true) + + @share_columns_check = Qt::Action.new(tr('&Share Columns'), self) + @share_columns_check.statusTip = tr('Share columns for items with the same name') + @share_columns_check.setCheckable(true) + + @unique_only_check = Qt::Action.new(tr('&Unique Only'), self) + @unique_only_check_keyseq = Qt::KeySequence.new(tr('Ctrl+U')) + @unique_only_check.shortcut = @unique_only_check_keyseq + @unique_only_check.statusTip = tr('Only output rows where data has changed') + @unique_only_check.setCheckable(true) + + @batch_mode_check = Qt::Action.new(tr('&Batch Mode'), self) + @batch_mode_check_keyseq = Qt::KeySequence.new(tr('Ctrl+B')) + @batch_mode_check.shortcut = @batch_mode_check_keyseq + @batch_mode_check.statusTip = tr('Process multiple config files with the same input files') + @batch_mode_check.setCheckable(true) + @batch_mode_check.connect(SIGNAL('triggered()')) { batch_mode_changed() } + + # Item Menu Actions + @item_edit = Qt::Action.new(tr('&Edit Items'), self) + @item_edit_keyseq = Qt::KeySequence.new(tr('Ctrl+E')) + @item_edit.shortcut = @item_edit_keyseq + @item_edit.statusTip = tr('Options') + @item_edit.connect(SIGNAL('triggered()')) { item_edit() } + + @item_delete = Qt::Action.new(tr('&Delete Items'), self) + @item_delete.statusTip = tr('Options') + @item_delete.connect(SIGNAL('triggered()')) { item_delete() } + end + + def initialize_menus + # File Menu + @file_menu = menuBar.addMenu(tr('&File')) + @file_menu.addAction(@open_config) + @file_menu.addAction(@save_config) + @file_menu.addSeparator() + @file_menu.addAction(@file_options) + @file_menu.addSeparator() + @file_menu.addAction(@exit_action) + + # Mode Menu + @mode_menu = menuBar.addMenu(tr('&Mode')) + @mode_menu.addAction(@fill_down_check) + @mode_menu.addAction(@matlab_header_check) + @mode_menu.addAction(@share_columns_check) + @mode_menu.addAction(@unique_only_check) + @mode_menu.addAction(@batch_mode_check) + + # Item Menu + @item_menu = menuBar.addMenu(tr('&Item')) + @item_menu.addAction(@item_edit) + @item_menu.addAction(@item_delete) + + # Help Menu + @about_string = "COSMOS Telemetry Extractor allows processing of telemetry log files and breaking out specified items." + initialize_help_menu() + end + + def initialize_central_widget + # Create the central widget + @central_widget = Qt::Widget.new + setCentralWidget(@central_widget) + + @top_layout = Qt::VBoxLayout.new + + @config_box = Qt::GroupBox.new("Configuration") + @config_box_layout = Qt::VBoxLayout.new + @config_box.setLayout(@config_box_layout) + @top_layout.addWidget(@config_box) + + # Configuration File Selector + @config_layout = Qt::HBoxLayout.new + @config_layout_label = Qt::Label.new('Configuration File:') + @config_layout.addWidget(@config_layout_label) + @config_field = Qt::LineEdit.new + @config_field.setReadOnly(true) + @config_layout.addWidget(@config_field) + @save_button = Qt::PushButton.new('Save...') + @config_layout.addWidget(@save_button) + @save_button.connect(SIGNAL('clicked()')) { handle_save_button() } + @browse_button = Qt::PushButton.new('Browse...') + @config_layout.addWidget(@browse_button) + @browse_button.connect(SIGNAL('clicked()')) { handle_browse_button() } + @config_box_layout.addLayout(@config_layout) + + # Separator before editor + @sep1 = Qt::Frame.new(@central_widget) + @sep1.setFrameStyle(Qt::Frame::HLine | Qt::Frame::Sunken) + @config_box_layout.addWidget(@sep1) + + # Configuration File Editor + @config_item_list = MyListWidget.new(self) + @config_item_list.setContextMenuPolicy(Qt::CustomContextMenu) + @config_item_list.setDragDropMode(Qt::AbstractItemView::InternalMove) + @config_item_list.setSelectionMode(Qt::AbstractItemView::ExtendedSelection) + @config_item_list.setMinimumHeight(150) + # Allow either double click or enter / return key to bring up the item editor + @config_item_list.connect(SIGNAL('itemDoubleClicked(QListWidgetItem*)')) { item_list_editor() } + @config_item_list.connect(SIGNAL('enterKeyPressed(int)')) { item_list_editor() } + connect(@config_item_list, SIGNAL('customContextMenuRequested(const QPoint&)'), self, SLOT('context_menu(const QPoint&)')) + @config_box_layout.addWidget(@config_item_list) + + # Telemetry Search + @search_layout = Qt::HBoxLayout.new + @search_box = FullTextSearchLineEdit.new(self) + @search_box.setStyleSheet("padding-right: 20px;padding-left: 5px;background: url(#{File.join(Cosmos::PATH, 'data', 'search-14.png')});background-position: right;background-repeat: no-repeat;") + @search_add_item_button = Qt::PushButton.new('Add Item') + @search_add_item_button.connect(SIGNAL('clicked()')) do + split_tlm = @search_box.text.to_s.split(" ") + if split_tlm.length == 3 + target_name = split_tlm[0].to_s.upcase + packet_name = split_tlm[1].to_s.upcase + item_name = split_tlm[2].to_s.upcase + begin + System.telemetry.packet_and_item(target_name, packet_name, item_name) + add_item_callback(target_name, packet_name, item_name) + rescue + # Does not exist + end + end + end + @search_add_packet_button = Qt::PushButton.new('Add Packet') + @search_add_packet_button.connect(SIGNAL('clicked()')) do + split_tlm = @search_box.text.to_s.split(" ") + if split_tlm.length >= 2 + target_name = split_tlm[0].to_s.upcase + packet_name = split_tlm[1].to_s.upcase + begin + System.telemetry.packet(target_name, packet_name) + add_packet_callback(target_name, packet_name) + rescue + # Does not exist + end + end + end + @search_add_target_button = Qt::PushButton.new('Add Target') + @search_add_target_button.connect(SIGNAL('clicked()')) do + split_tlm = @search_box.text.to_s.split(" ") + if split_tlm.length >= 1 + target_name = split_tlm[0].to_s.upcase + if System.telemetry.target_names.include?(target_name) + add_target_callback(target_name) + end + end + end + @search_layout.addWidget(@search_box) + @search_layout.addWidget(@search_add_item_button) + @search_layout.addWidget(@search_add_packet_button) + @search_layout.addWidget(@search_add_target_button) + @config_box_layout.addLayout(@search_layout) + + # Telemetry Chooser + tlm_chooser_layout = Qt::BoxLayout.new(Qt::Horizontal) + add_target_button = Qt::PushButton.new('Add Target') + add_target_button.connect(SIGNAL('clicked()')) do + add_target_callback(@telemetry_chooser.target_name) + end + tlm_chooser_layout.addWidget(add_target_button) + add_packet_button = Qt::PushButton.new('Add Packet') + add_packet_button.connect(SIGNAL('clicked()')) do + add_packet_callback(@telemetry_chooser.target_name, @telemetry_chooser.packet_name) + end + tlm_chooser_layout.addWidget(add_packet_button) + @telemetry_chooser = TelemetryChooser.new(self, Qt::Horizontal, true, true, false, true) + @telemetry_chooser.button_text = 'Add Item' + @telemetry_chooser.select_button_callback = method(:add_item_callback) + tlm_chooser_layout.addWidget(@telemetry_chooser) + @config_box_layout.addLayout(tlm_chooser_layout) + + # Text Item Chooser + @text_item_chooser = TextItemChooser.new(self) + @text_item_chooser.button_callback = method(:add_text_item_callback) + @config_box_layout.addWidget(@text_item_chooser) + + # Downsample + @downsample_entry = FloatChooser.new(self, 'Downsample Seconds:', 0.0, 0.0, nil, 20, true) + @config_box_layout.addWidget(@downsample_entry) + + # Batch Configuration + @batch_config_box = Qt::GroupBox.new("Batch Configuration") + @batch_config_layout = Qt::GridLayout.new + @batch_config_layout.setColumnStretch(1, 1) + @batch_config_box.setLayout(@batch_config_layout) + @top_layout.addWidget(@batch_config_box) + + row = 0 + + # Chooser for Log Files + label = Qt::Label.new('Config Files:') + @batch_config_layout.addWidget(label, row, 0) + @batch_fill_widget = Qt::Widget.new + @batch_fill_widget.setMinimumWidth(360) + @batch_config_layout.addWidget(@batch_fill_widget, row, 1, 0, 3) + @batch_browse_button = Qt::PushButton.new('Browse...') + @batch_browse_button.connect(SIGNAL('clicked()')) { handle_batch_browse_button() } + @batch_config_layout.addWidget(@batch_browse_button, row, 2) + @batch_remove_button = Qt::PushButton.new('Remove') + @batch_remove_button.connect(SIGNAL('clicked()')) { handle_batch_remove_button() } + @batch_config_layout.addWidget(@batch_remove_button, row, 3) + row += 1 + + @batch_filenames_entry = Qt::ListWidget.new(self) + @batch_filenames_entry.setSelectionMode(Qt::AbstractItemView::ExtendedSelection) + @batch_filenames_entry.setSortingEnabled(true) + @batch_filenames_entry.setMinimumHeight(90) + @batch_config_layout.addWidget(@batch_filenames_entry, row, 0, 3, 4) + row += 3 + + # Batch Name + @batch_name_label = Qt::Label.new('Batch Name:') + @batch_config_layout.addWidget(@batch_name_label, row, 0) + @batch_name_entry = Qt::LineEdit.new + @batch_name_entry.setMinimumWidth(340) + @batch_config_layout.addWidget(@batch_name_entry, row, 1, 1, 3) + row += 1 + + @batch_config_box.hide + + @file_box = Qt::GroupBox.new("File Selection") + @file_box_layout = Qt::VBoxLayout.new + @file_box.setLayout(@file_box_layout) + @top_layout.addWidget(@file_box) + + # Packet Log Frame + @packet_log_frame = PacketLogFrame.new(self, @log_dir, System.default_packet_log_reader.new, @input_filenames, nil, true, true, true, Cosmos::TLM_FILE_PATTERN, Cosmos::TXT_FILE_PATTERN) + @packet_log_frame.change_callback = method(:change_callback) + @file_box_layout.addWidget(@packet_log_frame) + + # Process and Open Buttons + @button_layout = Qt::HBoxLayout.new + + @process_button = Qt::PushButton.new('&Process Files') + @process_button.connect(SIGNAL('clicked()')) { process_log_files() } + @button_layout.addWidget(@process_button) + + @open_button = Qt::PushButton.new('&Open in Text Editor') + @open_button.connect(SIGNAL('clicked()')) { open_button() } + @open_button.setEnabled(false) + @button_layout.addWidget(@open_button) + + if Kernel.is_windows? + @open_excel_button = Qt::PushButton.new('&Open in Excel') + @open_excel_button.connect(SIGNAL('clicked()')) { open_excel_button() } + @open_excel_button.setEnabled(false) + @button_layout.addWidget(@open_excel_button) + end + @top_layout.addLayout(@button_layout) + + @central_widget.setLayout(@top_layout) + end # def initialize + + def self.post_options_parsed_hook(options) + if options.input_files + # Process config file + raise "Configuration File must be specified for command line processing" unless options.config_file + + # Process the input file(s) + tlm_extractor_config = TlmExtractorConfig.new(options.config_file) + tlm_extractor_processor = TlmExtractorProcessor.new + unless options.output_file + filename = options.input_files[0] + extension = File.extname(filename) + filename_no_extension = filename[0..-(extension.length + 1)] + if tlm_extractor_config.delimiter.to_s.strip == ',' + filename = filename_no_extension << '.csv' + else + filename = filename_no_extension << '.txt' + end + options.output_file = File.join(System.paths['LOGS'], File.basename(filename)) + end + + tlm_extractor_config.output_filename = options.output_file + tlm_extractor_processor.process(options.input_files, [tlm_extractor_config]) + puts "Created #{options.output_file}" + return false + else + return true + end + end + + # Runs the application + def self.run(option_parser = nil, options = nil) + Cosmos.catch_fatal_exception do + unless option_parser and options + option_parser, options = create_default_options() + options.width = 700 + options.height = 425 + options.auto_size = false + options.restore_size = false # always render this the correct size + options.title = "Telemetry Extractor" + option_parser.separator "Telemetry Extractor Specific Options:" + option_parser.on("-c", "--config FILE", "Use the specified configuration file") do |arg| + options.config_file = File.join(Cosmos::USERPATH, 'config', 'tools', 'tlm_extractor', arg) + end + option_parser.on("-i", "--input FILE", "Process the specified input file") do |arg| + options.input_files ||= [] + if arg[0..0] != '/' and arg[1..1] != ':' + # Relative path to default of log folder + arg = File.join(System.paths['LOGS'], arg) + end + options.input_files << arg + end + option_parser.on("-o", "--output FILE", "Output results to the specified file") do |arg| + options.output_file = arg + end + end + + super(option_parser, options) + end + end # def self.run + + def context_menu(point) + @item_menu.exec(@config_item_list.mapToGlobal(point)) + end + + protected + + def sync_gui_to_config + @tlm_extractor_config.matlab_header = @matlab_header_check.checked? + @tlm_extractor_config.fill_down = @fill_down_check.checked? + @tlm_extractor_config.share_columns = @share_columns_check.checked? + @tlm_extractor_config.unique_only = @unique_only_check.checked? + @tlm_extractor_config.downsample_seconds = @downsample_entry.value + @tlm_extractor_config.output_filename = @packet_log_frame.output_filename + + @tlm_extractor_config.clear_items + @config_item_list.each do |item| + split_item = item.text.scan ConfigParser::PARSING_REGEX + item_type = split_item[0] + target_name_or_column_name = split_item[1] + packet_name_or_text = split_item[2] + item_name = split_item[3] + value_type = split_item[4] + if value_type + value_type = value_type.upcase.intern + else + value_type = :CONVERTED + end + if item_type == 'ITEM' + @tlm_extractor_config.add_item(target_name_or_column_name, packet_name_or_text, item_name, value_type) + else + @tlm_extractor_config.add_text(target_name_or_column_name.remove_quotes, packet_name_or_text.remove_quotes) + end + end + end + + def sync_config_to_gui + @matlab_header_check.setChecked(@tlm_extractor_config.matlab_header) + @fill_down_check.setChecked(@tlm_extractor_config.fill_down) + @share_columns_check.setChecked(@tlm_extractor_config.share_columns) + @unique_only_check.setChecked(@tlm_extractor_config.unique_only) + @downsample_entry.value = @tlm_extractor_config.downsample_seconds + + clear_config_item_list() + @tlm_extractor_config.items.each do |item_type, target_name_or_column_name, packet_name_or_text, item_name, value_type| + if item_type == 'ITEM' + if value_type == :CONVERTED + @config_item_list.addItem("#{item_type} #{target_name_or_column_name} #{packet_name_or_text} #{item_name}") + else + @config_item_list.addItem("#{item_type} #{target_name_or_column_name} #{packet_name_or_text} #{item_name} #{value_type}") + end + else + @config_item_list.addItem("#{item_type} \"#{target_name_or_column_name}\" \"#{packet_name_or_text}\"") + end + end + end + + ############################################################################### + # File Menu Handlers + ############################################################################### + + # Handles processing log files + def process_log_files + @cancel = false + begin + @tlm_extractor_processor.packet_log_reader = @packet_log_frame.packet_log_reader + @input_filenames = @packet_log_frame.filenames.sort + @batch_filenames = [] + output_extension = '.txt' + batch_name = nil + if @batch_mode_check.checked? + batch_name = @batch_name_entry.text + @batch_filenames_entry.each {|list_item| @batch_filenames << list_item.text} + if @packet_log_frame.output_filename_filter == Cosmos::CSV_FILE_PATTERN + output_extension = '.csv' + else + output_extension = '.txt' + end + end + return unless pre_process_tests() + + # Configure Tlm Extractor Config + sync_gui_to_config() + + start_time = Time.now + ProgressDialog.execute(self, 'Log File Progress', 600, 300) do |progress_dialog| + progress_dialog.cancel_callback = method(:cancel_callback) + progress_dialog.enable_cancel_button + + begin + current_input_file_index = -1 + current_config_file_index = -1 + start_packet_count = -1 + last_packet_count = -1 + + if @batch_filenames.empty? + process_method = :process + process_args = [@input_filenames, [@tlm_extractor_config], @packet_log_frame.time_start, @packet_log_frame.time_end] + else + process_method = :process_batch + process_args = [batch_name, @input_filenames, @log_dir, output_extension, @batch_filenames, @packet_log_frame.time_start, @packet_log_frame.time_end] + end + + @tlm_extractor_processor.send(process_method, *process_args) do |input_file_index, packet_count, file_progress| + # Handle Cancel + break if @cancel + + # Handle Input File Change + if input_file_index != current_input_file_index + current_input_file_index = input_file_index + + if start_packet_count >= 0 + # Make sure some packets were found in the previous file + if packet_count == start_packet_count + # No packets found in previous file + progress_dialog.append_text(" WARNING: No packets processed in #{File.basename(@input_filenames[input_file_index - 1])}") + end + end + start_packet_count = packet_count + + progress_dialog.append_text("Processing File #{input_file_index + 1}/#{@input_filenames.length}: #{File.basename(@input_filenames[input_file_index])}") + progress_dialog.set_step_progress(0.0) + progress_dialog.set_overall_progress((input_file_index).to_f / @input_filenames.length.to_f) + end + + # Save packet_count + last_packet_count = packet_count + + # Handle Progress Reporting + progress_dialog.set_step_progress(file_progress) + end + # Make sure some packets were found in the previous file + if start_packet_count == last_packet_count + # No packets found in previous file + progress_dialog.append_text(" WARNING: No packets processed in #{File.basename(@input_filenames[-1])}") + end + + rescue => error + progress_dialog.append_text("Error processing:\n#{error.formatted}\n") + ensure + progress_dialog.set_step_progress(1.0) if !@cancel + progress_dialog.set_overall_progress(1.0) if !@cancel + progress_dialog.append_text("Runtime: #{Time.now - start_time} s") + progress_dialog.complete + if @batch_filenames.empty? + Qt.execute_in_main_thread(true) do + @open_button.setEnabled(true) + @open_excel_button.setEnabled(true) if Kernel.is_windows? + end + end + end + end # ProgressDialog.execute + rescue => error + Qt::MessageBox.critical(self, 'Error!', "Error Processing Log File(s)\n#{error.formatted}") + end + end # def process_log_files + + # Handles options dialog + def handle_options + box = Qt::Dialog.new(self) + box.setWindowTitle('Options') + top_layout = Qt::VBoxLayout.new + + delimiter_layout = Qt::HBoxLayout.new + delimiter_label = Qt::Label.new('Delimeter:') + delimiter_layout.addWidget(delimiter_label) + delimiter_field = Qt::LineEdit.new + delimiter_layout.addWidget(delimiter_field) + if @tlm_extractor_config.delimiter != "\t" + delimiter_field.setText(@tlm_extractor_config.delimiter) + else + delimiter_field.setText('tab') + end + top_layout.addLayout(delimiter_layout) + + checkbox_layout = Qt::HBoxLayout.new + checkbox_label = Qt::Label.new('Output filenames') + checkbox_layout.addWidget(checkbox_label) + checkbox_field = Qt::CheckBox.new + if @tlm_extractor_config.print_filenames_to_output + checkbox_field.setChecked(true) + else + checkbox_field.setChecked(false) + end + checkbox_layout.addWidget(checkbox_field) + top_layout.addLayout(checkbox_layout) + + button_layout = Qt::HBoxLayout.new + ok_button = Qt::PushButton.new('OK') + ok_button.connect(SIGNAL('clicked()')) { box.accept } + button_layout.addWidget(ok_button) + button_layout.addStretch + cancel_button = Qt::PushButton.new('CANCEL') + cancel_button.connect(SIGNAL('clicked()')) { box.reject } + button_layout.addWidget(cancel_button) + top_layout.addLayout(button_layout) + + box.setLayout(top_layout) + case box.exec + when Qt::Dialog::Accepted + delimiter = delimiter_field.text + if delimiter == 'tab' + delimiter = "\t" + end + @tlm_extractor_config.print_filenames_to_output = checkbox_field.checked? + @tlm_extractor_config.delimiter = delimiter + if @tlm_extractor_config.delimiter.to_s.strip == ',' + @packet_log_frame.output_filename_filter = Cosmos::CSV_FILE_PATTERN + else + @packet_log_frame.output_filename_filter = Cosmos::TXT_FILE_PATTERN + end + end + box.dispose + end + + def batch_mode_changed + if @batch_mode_check.checked? + @config_box.hide + @batch_config_box.show + @open_button.setEnabled(false) + @open_excel_button.setEnabled(false) if Kernel.is_windows? + @fill_down_check.setEnabled(false) + @matlab_header_check.setEnabled(false) + @share_columns_check.setEnabled(false) + @unique_only_check.setEnabled(false) + @open_config.setEnabled(false) + @save_config.setEnabled(false) + @file_options.setEnabled(false) + @item_edit.setEnabled(false) + @item_delete.setEnabled(false) + @packet_log_frame.select_output_dir + @packet_log_frame.output_filename = @log_dir + else + @config_box.show + @batch_config_box.hide + @fill_down_check.setEnabled(true) + @matlab_header_check.setEnabled(true) + @share_columns_check.setEnabled(true) + @unique_only_check.setEnabled(true) + @open_config.setEnabled(true) + @save_config.setEnabled(true) + @file_options.setEnabled(true) + @item_edit.setEnabled(true) + @item_delete.setEnabled(true) + @packet_log_frame.select_output_file + @packet_log_frame.output_filename = '' + end + end + + ############################################################################### + # Item Menu Handlers + ############################################################################### + + def item_edit + item_list_editor() + end + + def item_delete + @config_item_list.remove_selected_items + end + + ############################################################################### + # Handlers + ############################################################################### + + def handle_save_button + filename = nil + begin + if @config_field.text.strip.length > 0 + filename = Qt::FileDialog.getSaveFileName(self, "Save Config File", @config_field.text, "Config Files (*.txt);;All Files (*)") + else + filename = Qt::FileDialog.getSaveFileName(self, "Save Config File", @config_dir, "Config Files (*.txt);;All Files (*)") + end + if filename and filename.length != 0 + sync_gui_to_config() + @tlm_extractor_config.save(filename) + @config_field.setText(filename) + @config_dir = File.dirname(filename) + '/' + end + rescue => error + Qt::MessageBox.critical(self, 'Error!', "Error Saving Configuration File: #{filename}\n#{error.formatted}") + end + end + + def handle_browse_button + filename = Qt::FileDialog::getOpenFileName(self, "Open Config File:", @config_dir, "Config Files (*.txt);;All Files (*)") + if filename and not filename.empty? + @config_field.setText(filename) + @config_dir = File.dirname(filename) + '/' + + begin + process_config_file(filename) + rescue => error + Qt::MessageBox.critical(self, 'Error', "Error Processing Configuration File: #{filename}\n\n#{error.formatted}") + end + end + end + + def item_list_editor + done_items = false + selected_items = @config_item_list.selected_items() + if @config_item_list.currentItem and !selected_items.empty? + selected_items.each do |item_index| + dialog = Qt::Dialog.new(self) + dialog.setWindowTitle("Edit Item") + layout = Qt::VBoxLayout.new + split_item = @config_item_list.item(item_index).text.scan ConfigParser::PARSING_REGEX + + if split_item[0] == 'ITEM' and !done_items + label = Qt::Label.new("#{split_item[1]} #{split_item[2]} #{split_item[3]}") + layout.addWidget(label) + + label = Qt::Label.new('Value Type:') + layout.addWidget(label) + + box = Qt::ComboBox.new + box.maxCount = FORMATTING_OPTIONS.length + FORMATTING_OPTIONS.each {|item| box.addItem(item) } + current_formatting = split_item[4] + current_formatting = 'CONVERTED' unless current_formatting + if FORMATTING_OPTIONS.index(current_formatting) + box.currentIndex = FORMATTING_OPTIONS.index(current_formatting) + end + layout.addWidget(box) + + check_box = nil + if selected_items.length > 1 and item_index == selected_items[0] + check_box = Qt::CheckBox.new('Apply to All?') + layout.addWidget(check_box) + end + + button_layout = Qt::BoxLayout.new(Qt::Horizontal) + ok = Qt::PushButton.new("Save") + connect(ok, SIGNAL('clicked()'), dialog, SLOT('accept()')) + button_layout.addWidget(ok) + cancel = Qt::PushButton.new("Cancel") + connect(cancel, SIGNAL('clicked()'), dialog, SLOT('reject()')) + button_layout.addWidget(cancel) + layout.addLayout(button_layout) + + dialog.setLayout(layout) + if dialog.exec == Qt::Dialog::Accepted + if box.currentIndex != -1 + set_indexes = [item_index] + if check_box and check_box.checked? + set_indexes = selected_items + end + + set_indexes.each do |set_item_index| + split_item = @config_item_list.item(set_item_index).text.scan ConfigParser::PARSING_REGEX + if split_item[0] == 'ITEM' + # Remove any formatting from the item by only keeping the first four strings + @config_item_list.item(set_item_index).text = split_item[0..3].join(' ') + + if box.currentIndex != 0 + @config_item_list.item(set_item_index).text = "#{@config_item_list.item(set_item_index).text} #{FORMATTING_OPTIONS[box.currentIndex]}" + end + end + end + if set_indexes.length > 1 + done_items = true + end + end + end + elsif split_item[0] == 'TEXT' + label = Qt::Label.new('Column Name:') + layout.addWidget(label) + column_name = Qt::LineEdit.new(split_item[1].remove_quotes) + layout.addWidget(column_name) + label = Qt::Label.new('Text:') + layout.addWidget(label) + text = Qt::LineEdit.new(split_item[2].remove_quotes) + layout.addWidget(text) + + button_layout = Qt::BoxLayout.new(Qt::Horizontal) + ok = Qt::PushButton.new("Save") + connect(ok, SIGNAL('clicked()'), dialog, SLOT('accept()')) + button_layout.addWidget(ok) + cancel = Qt::PushButton.new("Cancel") + connect(cancel, SIGNAL('clicked()'), dialog, SLOT('reject()')) + button_layout.addWidget(cancel) + layout.addLayout(button_layout) + + dialog.setLayout(layout) + if dialog.exec == Qt::Dialog::Accepted + # Remove any formatting from the item by only keeping the first four strings + @config_item_list.item(item_index).text = "#{split_item[0]} \"#{column_name.text}\" \"#{text.text}\"" + end + end + + dialog.dispose + end + end + end + + def open_button + Cosmos.open_in_text_editor(@packet_log_frame.output_filename) + end + + def open_excel_button + system("start Excel.exe \"#{@packet_log_frame.output_filename}\"") + end + + def clear_config_item_list + @config_item_list.clearItems + end # def clear_data_object_list + + # Handles removing a selected filename + def handle_batch_remove_button + @batch_filenames_entry.remove_selected_items + end + + # Handles browsing for log files + def handle_batch_browse_button + Cosmos.set_working_dir do + filenames = Qt::FileDialog::getOpenFileNames( + self, "Select Config File(s):", @config_dir, Cosmos::TXT_FILE_PATTERN) + if filenames and not filenames.empty? + @config_dir = File.dirname(filenames[0]) + '/' + filenames.each {|filename| @batch_filenames_entry.addItem(filename) if @batch_filenames_entry.findItems(filename, Qt::MatchExactly).empty? } + end + end + end + + ############################################################################### + # Additional Callbacks + ############################################################################### + + def cancel_callback(progress_dialog = nil) + @cancel = true + return true, false + end + + def add_target_callback(target_name) + packets = System.telemetry.packets(target_name) + packets.each do |packet_name, packet| + packet.sorted_items.each do |item| + @config_item_list.addItem("ITEM #{target_name} #{packet_name} #{item.name}") + end + end + end + + def add_packet_callback(target_name, packet_name) + packet = System.telemetry.packet(target_name, packet_name) + packet.sorted_items.each do |item| + @config_item_list.addItem("ITEM #{target_name} #{packet_name} #{item.name}") + end + end + + def add_item_callback(target_name, packet_name, item_name) + @config_item_list.addItem("ITEM #{target_name} #{packet_name} #{item_name}") + end + + def add_text_item_callback(column_name, text) + @config_item_list.addItem("TEXT \"#{column_name}\" \"#{text}\"") + end + + ############################################################################### + # Helper Methods + ############################################################################### + + def pre_process_tests + if !@batch_mode_check.checked? + # Normal Mode + + if @config_item_list.count < 1 + Qt::MessageBox.critical(self, 'Error', 'Please select at least 1 item') + return false + end + + unless @packet_log_frame.output_filename + Qt::MessageBox.critical(self, 'Error', 'No Output File Selected') + return false + end + + if File.exist?(@packet_log_frame.output_filename) + result = Qt::MessageBox.warning(self, 'Warning!', 'Output File Already Exists. Overwrite?', Qt::MessageBox::Yes | Qt::MessageBox::No) + return false if result == Qt::MessageBox::No + end + else + # Batch Mode + + if !@batch_name_entry.text or @batch_name_entry.text.strip.empty? + Qt::MessageBox.critical(self, 'Error', 'Batch Name is Required') + return false + end + + unless @batch_filenames and @batch_filenames[0] + Qt::MessageBox.critical(self, 'Error', 'Please select at least 1 config file') + return false + end + end + + unless @input_filenames and @input_filenames[0] + Qt::MessageBox.critical(self, 'Error', 'Please select at least 1 input file') + return false + end + + # Validate configurations exist for input filenames + @input_filenames.each do |input_filename| + Cosmos.check_log_configuration(@tlm_extractor_processor.packet_log_reader, input_filename) + end + + #Validate config information + @tlm_extractor_processor.packet_log_reader.open(@input_filenames[0]) + @tlm_extractor_processor.packet_log_reader.close + + @config_item_list.each do |item| + split_item = item.text.split + item_type = split_item[0] + target_name = split_item[1] + packet_name = split_item[2] + item_name = split_item[3] + + if item_type == 'ITEM' + # Verify Packet + packet = nil + begin + packet = System.telemetry.packet(target_name, packet_name) + rescue + Qt::MessageBox.critical(self, 'Error!', "Unknown Packet #{target_name} #{packet_name} specified") + return false + end + + # Verify Item + begin + packet.get_item(item_name) + rescue + Qt::MessageBox.critical(self, 'Error!', "Item #{item_name} not present in packet") + return false + end + end + end + + true + end + + def process_config_file(filename) + @tlm_extractor_config.restore(filename) + sync_config_to_gui() + end + + def change_callback(item_changed) + if item_changed == :INPUT_FILES + if !@batch_mode_check.checked? + filename = @packet_log_frame.filenames[0] + if filename + extension = File.extname(filename) + filename_no_extension = filename[0..-(extension.length + 1)] + if @packet_log_frame.output_filename_filter == Cosmos::CSV_FILE_PATTERN + filename = filename_no_extension << '.csv' + else + filename = filename_no_extension << '.txt' + end + @packet_log_frame.output_filename = filename + end + end + elsif item_changed == :OUTPUT_FILE + output_filename = @packet_log_frame.output_filename + if output_filename and !output_filename.to_s.strip.empty? + @log_dir = File.dirname(output_filename) + end + elsif item_changed == :OUTPUT_DIR + output_filename = @packet_log_frame.output_filename + if output_filename and !output_filename.to_s.strip.empty? + @log_dir = output_filename + end + end + end + + end # class TlmExtractor + +end # module Cosmos