diff --git a/demo/config/tools/cmd_tlm_server/cmd_tlm_server.txt b/demo/config/tools/cmd_tlm_server/cmd_tlm_server.txt index d45641881..95532d6a9 100644 --- a/demo/config/tools/cmd_tlm_server/cmd_tlm_server.txt +++ b/demo/config/tools/cmd_tlm_server/cmd_tlm_server.txt @@ -42,4 +42,4 @@ BACKGROUND_TASK example_background_task.rb STOPPED BACKGROUND_TASK limits_groups.rb 5 # Initial delay to allow interfaces to connect -COLLECT_METADATA +#COLLECT_METADATA diff --git a/demo/procedures/replay_test.rb b/demo/procedures/replay_test.rb new file mode 100644 index 000000000..e0eddf940 --- /dev/null +++ b/demo/procedures/replay_test.rb @@ -0,0 +1,32 @@ +set_line_delay(0) +set_replay_mode(true) +filenames = get_output_logs_filenames() +replay_select_file(filenames[-1]) +status = 'Analyzing' +while status =~ /Analyzing/ + status, playback_delay, filename, file_start, file_current, file_end, file_index, file_max_index = replay_status + wait(1) +end +100.times do + replay_step_forward() +end +100.times do + replay_step_back() +end +replay_move_end() +replay_move_index(file_max_index / 2) +replay_move_start() +replay_set_playback_delay(0.1) +replay_play() +wait(2) +replay_set_playback_delay(0.125) +wait(5) +replay_set_playback_delay(nil) +wait(2) +replay_set_playback_delay(0.0) +wait(4) +replay_stop() +replay_reverse_play() +wait(5) +cmd_tlm_clear_counters() +cmd_tlm_reload() diff --git a/ext/cosmos/ext/structure/structure.c b/ext/cosmos/ext/structure/structure.c index a7c22ab07..0ecbb6f87 100644 --- a/ext/cosmos/ext/structure/structure.c +++ b/ext/cosmos/ext/structure/structure.c @@ -64,6 +64,7 @@ static ID id_ivar_neg_bit_size = 0; static ID id_ivar_fixed_size = 0; static ID id_ivar_short_buffer_allowed = 0; static ID id_ivar_mutex = 0; +static ID id_ivar_create_index = 0; static ID id_const_ASCII_8BIT_STRING = 0; static ID id_const_ZERO_STRING = 0; @@ -1189,6 +1190,16 @@ static VALUE structure_item_spaceship(VALUE self, VALUE other_item) { int other_bit_offset = FIX2INT(rb_ivar_get(other_item, id_ivar_bit_offset)); int bit_size = 0; int other_bit_size = 0; + int create_index = 0; + int other_create_index = 0; + int have_create_index = 0; + volatile VALUE v_create_index = rb_ivar_get(self, id_ivar_create_index); + volatile VALUE v_other_create_index = rb_ivar_get(other_item, id_ivar_create_index); + if (RTEST(v_create_index) && RTEST(v_other_create_index)) { + create_index = FIX2INT(v_create_index); + other_create_index = FIX2INT(v_other_create_index); + have_create_index = 1; + } /* Handle same bit offset case */ if ((bit_offset == 0) && (other_bit_offset == 0)) { @@ -1198,7 +1209,15 @@ static VALUE structure_item_spaceship(VALUE self, VALUE other_item) { bit_size = FIX2INT(rb_ivar_get(self, id_ivar_bit_size)); other_bit_size = FIX2INT(rb_ivar_get(other_item, id_ivar_bit_size)); if (bit_size == other_bit_size) { - return INT2FIX(0); + if (have_create_index) { + if (create_index <= other_create_index) { + return INT2FIX(-1); + } else { + return INT2FIX(1); + } + } else { + return INT2FIX(0); + } } if (bit_size < other_bit_size) { return INT2FIX(-1); } else { @@ -1210,7 +1229,15 @@ static VALUE structure_item_spaceship(VALUE self, VALUE other_item) { if (((bit_offset >= 0) && (other_bit_offset >= 0)) || ((bit_offset < 0) && (other_bit_offset < 0))) { /* Both Have Same Sign */ if (bit_offset == other_bit_offset) { - return INT2FIX(0); + if (have_create_index) { + if (create_index <= other_create_index) { + return INT2FIX(-1); + } else { + return INT2FIX(1); + } + } else { + return INT2FIX(0); + } } else if (bit_offset < other_bit_offset) { return INT2FIX(-1); } else { @@ -1219,7 +1246,15 @@ static VALUE structure_item_spaceship(VALUE self, VALUE other_item) { } else { /* Different Signs */ if (bit_offset == other_bit_offset) { - return INT2FIX(0); + if (have_create_index) { + if (create_index <= other_create_index) { + return INT2FIX(-1); + } else { + return INT2FIX(1); + } + } else { + return INT2FIX(0); + } } else if (bit_offset < other_bit_offset) { return INT2FIX(1); } else { @@ -1387,6 +1422,7 @@ void Init_structure (void) id_ivar_fixed_size = rb_intern("@fixed_size"); id_ivar_short_buffer_allowed = rb_intern("@short_buffer_allowed"); id_ivar_mutex = rb_intern("@mutex"); + id_ivar_create_index = rb_intern("@create_index"); symbol_LITTLE_ENDIAN = ID2SYM(rb_intern("LITTLE_ENDIAN")); symbol_BIG_ENDIAN = ID2SYM(rb_intern("BIG_ENDIAN")); diff --git a/lib/cosmos/gui/qt.rb b/lib/cosmos/gui/qt.rb index 5145f6557..6c5fab641 100644 --- a/lib/cosmos/gui/qt.rb +++ b/lib/cosmos/gui/qt.rb @@ -252,6 +252,7 @@ def self.check_log_configuration(packet_log_reader, log_filename) end end end + return config_change_success, change_error end end diff --git a/lib/cosmos/interfaces/protocols/fixed_protocol.rb b/lib/cosmos/interfaces/protocols/fixed_protocol.rb index 22602bd7e..b2720656f 100644 --- a/lib/cosmos/interfaces/protocols/fixed_protocol.rb +++ b/lib/cosmos/interfaces/protocols/fixed_protocol.rb @@ -53,7 +53,7 @@ def read_packet(packet) # Identifies an unknown buffer of data as a Packet. The raw data is # returned but the packet that matched is recorded so it can be set in the - # post_read_packet callback. + # read_packet callback. # # @return [String|Symbol] The identified packet data or :STOP if more data # is required to build a packet @@ -82,7 +82,7 @@ def identify_and_finish_packet return :STOP if @data.length < identified_packet.defined_length end # Set some variables so we can update the packet in - # post_read_packet + # read_packet @received_time = Time.now.sys @target_name = identified_packet.target_name @packet_name = identified_packet.packet_name diff --git a/lib/cosmos/packets/structure.rb b/lib/cosmos/packets/structure.rb index 9761e0be5..159c8f3c7 100644 --- a/lib/cosmos/packets/structure.rb +++ b/lib/cosmos/packets/structure.rb @@ -198,7 +198,7 @@ def define(item) # to re-sort. We also re-sort if the current item is less than the less # item because we are inserting. if last_item.bit_offset <= 0 or item.bit_offset <= 0 or item.bit_offset < last_item.bit_offset - @sorted_items = @sorted_items.sort {|item1, item2| item1 <=> item2} + @sorted_items = @sorted_items.sort end else @sorted_items << item diff --git a/lib/cosmos/packets/structure_item.rb b/lib/cosmos/packets/structure_item.rb index 0a98efb18..7d5b783c6 100644 --- a/lib/cosmos/packets/structure_item.rb +++ b/lib/cosmos/packets/structure_item.rb @@ -17,6 +17,8 @@ module Cosmos class StructureItem include Comparable + @@create_index = 0 + # Valid data types adds :DERIVED to those defined by BinaryAccessor DATA_TYPES = BinaryAccessor::DATA_TYPES << :DERIVED @@ -81,6 +83,8 @@ def initialize(name, bit_offset, bit_size, data_type, endianness, array_size = n self.bit_size = bit_size self.array_size = array_size self.overflow = overflow + @create_index = @@create_index + @@create_index += 1 @structure_item_constructed = true verify_overall() end @@ -190,6 +194,10 @@ def overflow=(overflow) verify_overall() if @structure_item_constructed end + def create_index + @create_index.to_i + end + if RUBY_ENGINE != 'ruby' or ENV['COSMOS_NO_EXT'] # Comparison Operator based on bit_offset. This means that StructureItems # with different names or bit sizes are equal if they have the same bit @@ -202,9 +210,17 @@ def <=>(other_item) if (@bit_offset == 0) && (other_bit_offset == 0) # Both bit_offsets are 0 so sort by bit_size # This allows derived items with bit_size of 0 to be listed first - # Compare based on bit size + # Compare based on bit size then create index if @bit_size == other_bit_size - return 0 + if @create_index + if @create_index <= other_item.create_index + return -1 + else + return 1 + end + else + return 0 + end elsif @bit_size < other_bit_size return -1 else @@ -216,8 +232,16 @@ def <=>(other_item) if ((@bit_offset >= 0) && (other_bit_offset >= 0)) || ((@bit_offset < 0) && (other_bit_offset < 0)) # Both Have Same Sign if @bit_offset == other_bit_offset - return 0 - elsif @bit_offset < other_bit_offset + if @create_index + if @create_index <= other_item.create_index + return -1 + else + return 1 + end + else + return 0 + end + elsif @bit_offset <= other_bit_offset return -1 else return 1 @@ -225,7 +249,15 @@ def <=>(other_item) else # Different Signs if @bit_offset == other_bit_offset - return 0 + if @create_index + if @create_index < other_item.create_index + return -1 + else + return 1 + end + else + return 0 + end elsif @bit_offset < other_bit_offset return 1 else diff --git a/lib/cosmos/script/cmd_tlm_server.rb b/lib/cosmos/script/cmd_tlm_server.rb index 1f9edcf35..78686803c 100644 --- a/lib/cosmos/script/cmd_tlm_server.rb +++ b/lib/cosmos/script/cmd_tlm_server.rb @@ -196,5 +196,17 @@ def unsubscribe_server_messages(id) def get_server_message(id, non_block = false) return $cmd_tlm_server.get_server_message(id, non_block) end + + def cmd_tlm_reload + return $cmd_tlm_server.cmd_tlm_reload + end + + def cmd_tlm_clear_counters + return $cmd_tlm_server.cmd_tlm_clear_counters + end + + def get_output_logs_filenames(filter = '*tlm.bin') + return $cmd_tlm_server.get_output_logs_filenames(filter) + end end end diff --git a/lib/cosmos/script/replay.rb b/lib/cosmos/script/replay.rb new file mode 100644 index 000000000..ca945bdf6 --- /dev/null +++ b/lib/cosmos/script/replay.rb @@ -0,0 +1,60 @@ +# encoding: ascii-8bit + +# Copyright 2017 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 + +module Cosmos + + module Script + private + + def replay_select_file(filename, packet_log_reader = "DEFAULT") + $cmd_tlm_server.replay_select_file(filename, packet_log_reader) + end + + def replay_status + $cmd_tlm_server.replay_status + end + + def replay_set_playback_delay(delay) + $cmd_tlm_server.replay_set_playback_delay(delay) + end + + def replay_play + $cmd_tlm_server.replay_play + end + + def replay_reverse_play + $cmd_tlm_server.replay_reverse_play + end + + def replay_stop + $cmd_tlm_server.replay_stop + end + + def replay_step_forward + $cmd_tlm_server.replay_step_forward + end + + def replay_step_back + $cmd_tlm_server.replay_step_back + end + + def replay_move_start + $cmd_tlm_server.replay_move_start + end + + def replay_move_end + $cmd_tlm_server.replay_move_end + end + + def replay_move_index(index) + $cmd_tlm_server.replay_move_index(index) + end + end +end diff --git a/lib/cosmos/script/script.rb b/lib/cosmos/script/script.rb index fd0296cc4..af803450e 100644 --- a/lib/cosmos/script/script.rb +++ b/lib/cosmos/script/script.rb @@ -12,6 +12,7 @@ require 'cosmos/io/json_drb_object' require 'cosmos/tools/cmd_tlm_server/cmd_tlm_server' require 'cosmos/script/cmd_tlm_server' +require 'cosmos/script/replay' require 'cosmos/script/commands' require 'cosmos/script/telemetry' require 'cosmos/script/limits' @@ -20,6 +21,7 @@ $cmd_tlm_server = nil $cmd_tlm_disconnect = false +$cmd_tlm_replay_mode = false module Cosmos class CheckError < RuntimeError; end @@ -39,6 +41,7 @@ module Script # Called when this module is mixed in using "include Cosmos::Script" def self.included(base) $cmd_tlm_disconnect = false + $cmd_tlm_replay_mode = false $cmd_tlm_server = nil initialize_script_module() end @@ -48,8 +51,12 @@ def initialize_script_module(config_file = CmdTlmServer::DEFAULT_CONFIG_FILE) # Start up a standalone CTS in disconnected mode $cmd_tlm_server = CmdTlmServer.new(config_file, false, true) else - # Start a Json connect to the real CTS server - $cmd_tlm_server = JsonDRbObject.new(System.connect_hosts['CTS_API'], System.ports['CTS_API']) + # Start a Json connect to the real server + if $cmd_tlm_replay_mode + $cmd_tlm_server = JsonDRbObject.new(System.connect_hosts['REPLAY_API'], System.ports['REPLAY_API']) + else + $cmd_tlm_server = JsonDRbObject.new(System.connect_hosts['CTS_API'], System.ports['CTS_API']) + end end end @@ -72,5 +79,16 @@ def script_disconnect $cmd_tlm_server.disconnect if $cmd_tlm_server && !$cmd_tlm_disconnect end + def set_replay_mode(replay_mode) + if replay_mode != $cmd_tlm_replay_mode + $cmd_tlm_replay_mode = replay_mode + initialize_script_module() + end + end + + def get_replay_mode + $cmd_tlm_replay_mode + end + end end diff --git a/lib/cosmos/script/tools.rb b/lib/cosmos/script/tools.rb index 4595efcb7..0993a6f68 100644 --- a/lib/cosmos/script/tools.rb +++ b/lib/cosmos/script/tools.rb @@ -92,6 +92,20 @@ def show_backtrace(value = true) end end + ########################### + # Telemetry Screen methods + ########################### + + # Get the organized list of available telemetry screens + def get_screen_list(config_filename = nil, force_refresh = false) + $cmd_tlm_server.get_screen_list(config_filename, force_refresh) + end + + # Get a specific screen definition + def get_screen_definition(screen_full_name, config_filename = nil, force_refresh = false) + $cmd_tlm_server.get_screen_definition(screen_full_name, config_filename, force_refresh) + end + end # module Script end # module Cosmos diff --git a/lib/cosmos/system/system.rb b/lib/cosmos/system/system.rb index 82fff5440..bda271359 100644 --- a/lib/cosmos/system/system.rb +++ b/lib/cosmos/system/system.rb @@ -73,9 +73,9 @@ class System instance_attr_reader :additional_md5_files # Known COSMOS ports - KNOWN_PORTS = ['CTS_API', 'TLMVIEWER_API', 'CTS_PREIDENTIFIED', 'CTS_CMD_ROUTER'] + KNOWN_PORTS = ['CTS_API', 'TLMVIEWER_API', 'CTS_PREIDENTIFIED', 'CTS_CMD_ROUTER', 'REPLAY_API', 'REPLAY_PREIDENTIFIED', 'REPLAY_CMD_ROUTER'] # Known COSMOS hosts - KNOWN_HOSTS = ['CTS_API', 'TLMVIEWER_API', 'CTS_PREIDENTIFIED', 'CTS_CMD_ROUTER'] + KNOWN_HOSTS = ['CTS_API', 'TLMVIEWER_API', 'CTS_PREIDENTIFIED', 'CTS_CMD_ROUTER', 'REPLAY_API', 'REPLAY_PREIDENTIFIED', 'REPLAY_CMD_ROUTER'] # Known COSMOS paths KNOWN_PATHS = ['LOGS', 'TMP', 'SAVED_CONFIG', 'TABLES', 'HANDBOOKS', 'PROCEDURES', 'SEQUENCES'] @@ -90,69 +90,7 @@ class System # read. Be default this is /config/system/system.txt def initialize(filename = nil) raise "Cosmos::System created twice" unless @@instance.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 - - @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' - - @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' - - @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') - - 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 + reset_variables(filename) @@instance = self end @@ -491,6 +429,8 @@ def process_targets(parser, filename, configuration_directory) 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. @@ -545,6 +485,96 @@ def self.load_configuration(name = nil) return self.instance.load_configuration(name) end + # Resets the System's internal state to defaults. + # + # @params [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 + + @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' + + @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' + + @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') + + 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 + protected def unzip(zip_file) @@ -586,7 +616,11 @@ def recursively_deflate_directory(disk_file_path, io, zip_file_path) def put_into_archive(disk_file_path, io, zip_file_path) io.get_output_stream(zip_file_path) do |f| - f.write(File.open(disk_file_path, 'rb').read) + data = nil + File.open(disk_file_path, 'rb') do |file| + data = file.read + end + f.write(data) end end diff --git a/lib/cosmos/tools/cmd_tlm_server/api.rb b/lib/cosmos/tools/cmd_tlm_server/api.rb index f63025ab6..6bd3b0e87 100644 --- a/lib/cosmos/tools/cmd_tlm_server/api.rb +++ b/lib/cosmos/tools/cmd_tlm_server/api.rb @@ -10,6 +10,7 @@ require 'cosmos/script/extract' require 'cosmos/script/api_shared' +require 'cosmos/tools/tlm_viewer/tlm_viewer_config' module Cosmos @@ -119,7 +120,26 @@ def initialize 'start_raw_logging_router', 'stop_raw_logging_router', 'get_server_message_log_filename', - 'start_new_server_message_log'] + 'start_new_server_message_log', + 'cmd_tlm_reload', + 'cmd_tlm_clear_counters', + 'get_output_logs_filenames', + 'replay_select_file', + 'replay_status', + 'replay_set_playback_delay', + 'replay_play', + 'replay_reverse_play', + 'replay_stop', + 'replay_step_forward', + 'replay_step_back', + 'replay_move_start', + 'replay_move_end', + 'replay_move_index', + 'get_screen_list', + 'get_screen_definition', + ] + @tlm_viewer_config_filename = nil + @tlm_viewer_config = nil end ############################################################################ @@ -1313,7 +1333,7 @@ def stop_background_task(task_name) # number of Ruby threads in the server/ def get_server_status [ System.limits_set.to_s, - System.ports['CTS_API'], + CmdTlmServer.mode == :CMD_TLM_SERVER ? System.ports['CTS_API'] : System.ports['REPLAY_API'], CmdTlmServer.json_drb.num_clients, CmdTlmServer.json_drb.request_count, CmdTlmServer.json_drb.average_request_time, @@ -1429,15 +1449,116 @@ def stop_raw_logging_router(router_name = 'ALL') # @return [String] The server message log filename def get_server_message_log_filename - CmdTlmServer.message_log.filename + if CmdTlmServer.message_log + CmdTlmServer.message_log.filename + else + nil + end end # Starts a new server message log def start_new_server_message_log - CmdTlmServer.message_log.start + CmdTlmServer.message_log.start if CmdTlmServer.message_log nil end + # Reload the default configuration + def cmd_tlm_reload + CmdTlmServer.instance.reload + end + + # Clear server counters + def cmd_tlm_clear_counters + CmdTlmServer.clear_counters + end + + # Get the list of filenames in the outputs logs folder + def get_output_logs_filenames(filter = '*tlm.bin') + raise "Filter must not contain slashes" if filter.index('/') or filter.index('\\') + Dir.glob(File.join(System.paths['LOGS'], '**', filter)) + end + + # Select and start analyzing a file for replay + # + # filename [String] filename relative to output logs folder or absolute filename + def replay_select_file(filename, packet_log_reader = "DEFAULT") + CmdTlmServer.replay_backend.select_file(filename, packet_log_reader) + end + + # Get current replay status + # + # status, delay, filename, file_start, file_current, file_end, file_index, file_max_index + def replay_status + CmdTlmServer.replay_backend.status + end + + # Set the replay delay + # + # @param delay [Float] delay between packets in seconds 0.0 to 1.0, nil = REALTIME + def replay_set_playback_delay(delay) + CmdTlmServer.replay_backend.set_playback_delay(delay) + end + + # Replay start playing forward + def replay_play + CmdTlmServer.replay_backend.play + end + + # Replay start playing backward + def replay_reverse_play + CmdTlmServer.replay_backend.reverse_play + end + + # Replay stop + def replay_stop + CmdTlmServer.replay_backend.stop + end + + # Replay step forward one packet + def replay_step_forward + CmdTlmServer.replay_backend.step_forward + end + + # Replay step backward one packet + def replay_step_back + CmdTlmServer.replay_backend.step_back + end + + # Replay move to start of file + def replay_move_start + CmdTlmServer.replay_backend.move_start + end + + # Replay move to end of file + def replay_move_end + CmdTlmServer.replay_backend.move_end + end + + # Replay move to index + # + # @param index [Integer] packet index into file + def replay_move_index(index) + CmdTlmServer.replay_backend.move_index(index) + end + + # Get the organized list of available telemetry screens + def get_screen_list(config_filename = nil, force_refresh = false) + if force_refresh or !@tlm_viewer_config or @tlm_viewer_config_filename != config_filename + @tlm_viewer_config = TlmViewerConfig.new(config_filename, true) + @tlm_viewer_config_filename = config_filename + end + return @tlm_viewer_config.columns + end + + # Get a specific screen definition + def get_screen_definition(screen_full_name, config_filename = nil, force_refresh = false) + get_screen_list(config_filename, force_refresh) if force_refresh or !@tlm_viewer_config + screen_info = @tlm_viewer_config.screen_infos[screen_full_name.upcase] + raise "Unknown screen: #{screen_full_name.upcase}" unless screen_info + screen_definition = File.read(screen_info.filename) + return screen_definition + end + private def cmd_implementation(range_check, hazardous_check, raw, method_name, *args) diff --git a/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server.rb b/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server.rb index 0ea16b4e1..4f958f35a 100644 --- a/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server.rb +++ b/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server.rb @@ -17,6 +17,7 @@ require 'cosmos/tools/cmd_tlm_server/interfaces' require 'cosmos/tools/cmd_tlm_server/packet_logging' require 'cosmos/tools/cmd_tlm_server/routers' +require 'cosmos/tools/cmd_tlm_server/replay_backend' module Cosmos @@ -39,6 +40,8 @@ class CmdTlmServer instance_attr_reader :packet_logging # @return [Routers] Access to the routers instance_attr_reader :routers + # @return [ReplayBackend] Access to replay logic + instance_attr_reader :replay_backend # @return [MessageLog] Message log for the CmdTlmServer instance_attr_reader :message_log # @return [JsonDRb] Provides access to the server for all tools both @@ -46,6 +49,8 @@ class CmdTlmServer instance_attr_accessor :json_drb # @return [String] CmdTlmServer title as set in the config file instance_attr_accessor :title + # @return [Symbol] mode :CMD_TLM_SERVER or :REPLAY + instance_attr_accessor :mode # attr_reader attributes are only used by CmdTlmServer internally and are # thus only available as attributes on the singleton @@ -108,7 +113,8 @@ def self.meta_callback= (meta_callback) @@meta_callback = meta_callback end - # Constructor for a CmdTlmServer + # Constructor for a CmdTlmServer. Initializes all internal state and + # starts up the sever # # @param config_file [String] The name of the server configuration file # which must be in the config/tools/cmd_tlm_server directory. @@ -119,13 +125,16 @@ def self.meta_callback= (meta_callback) # stand-alone mode which does not actually use the interfaces to send and # receive data. This is useful for testing scripts when actual hardware # is not available. - def initialize(config_file = DEFAULT_CONFIG_FILE, - production = false, - disconnect = false, - create_message_log = true) + # @param mode [Symbol] :CMD_TLM_SERVER or :REPLAY - Defines overall mode + def initialize( + config_file = DEFAULT_CONFIG_FILE, + production = false, + disconnect = false, + mode = :CMD_TLM_SERVER) + @@instance = self @packet_logging = nil # Removes warnings - @message_log = MessageLog.new('server') if create_message_log + @mode = mode super() # For Api @@ -150,8 +159,13 @@ def initialize(config_file = DEFAULT_CONFIG_FILE, @interfaces = Interfaces.new(@config, method(:identified_packet_callback)) @packet_logging = PacketLogging.new(@config) @routers = Routers.new(@config) + @replay_backend = ReplayBackend.new(@config) @title = @config.title + if @mode != :CMD_TLM_SERVER + @title.gsub!("Command and Telemetry Server", "Replay") + end @stop_callback = nil + @reload_callback = nil # Set Threads to kill CTS if they throw an exception Thread.abort_on_exception = true @@ -159,113 +173,82 @@ def initialize(config_file = DEFAULT_CONFIG_FILE, # Don't start the DRb service or the telemetry monitoring thread # if we started the server in disconnect mode @json_drb = nil - start(production) unless @disconnect - end # end def initialize - - # Start up the system by starting the JSON-RPC server, interfaces, routers, - # and background tasks. Starts a thread to monitor all packets for - # staleness so other tools (such as Packet Viewer or Telemetry Viewer) can - # react accordingly. - # - # @param production (see #initialize) - def start(production = false) - System.telemetry # Make sure definitions are loaded by starting anything - return unless @json_drb.nil? - - @@meta_callback.call() if @@meta_callback if @config.metadata - - # Start DRb with access control - @json_drb = JsonDRb.new - @json_drb.acl = System.acl if System.acl - - # In production we start logging and don't allow the user to stop it - # We also disallow setting telemetry and disconnecting from interfaces - if production - @packet_logging.start - @api_whitelist.delete('stop_logging') - @api_whitelist.delete('stop_cmd_log') - @api_whitelist.delete('stop_tlm_log') - @interfaces.all.each do |name, interface| - interface.disable_disconnect = true - end - @routers.all.each do |name, interface| - interface.disable_disconnect = true + unless @disconnect + System.telemetry # Make sure definitions are loaded by starting anything + + @@meta_callback.call() if @@meta_callback and @config.metadata + + # Start DRb with access control + @json_drb = JsonDRb.new + @json_drb.acl = System.acl if System.acl + + # In production we start logging and don't allow the user to stop it + # We also disallow setting telemetry and disconnecting from interfaces + if production + @api_whitelist.delete('stop_logging') + @api_whitelist.delete('stop_cmd_log') + @api_whitelist.delete('stop_tlm_log') + @interfaces.all.each do |name, interface| + interface.disable_disconnect = true + end + @routers.all.each do |name, interface| + interface.disable_disconnect = true + end end - end - @json_drb.method_whitelist = @api_whitelist - begin - @json_drb.start_service(System.listen_hosts['CTS_API'], System.ports['CTS_API'], self) - rescue Exception - # Call packet_logging shutdown here to explicitly kill the logging - # threads since this CTS is not going to launch - @packet_logging.shutdown - raise FatalError.new("Error starting JsonDRb on port #{System.ports['CTS_API']}.\nPerhaps a Command and Telemetry Server is already running?") - end - - @routers.add_preidentified('PREIDENTIFIED_ROUTER', System.instance.ports['CTS_PREIDENTIFIED']) - @routers.add_cmd_preidentified('PREIDENTIFIED_CMD_ROUTER', System.instance.ports['CTS_CMD_ROUTER']) - System.telemetry.limits_change_callback = method(:limits_change_callback) - @interfaces.start - @routers.start - @background_tasks.start_all - - # Start staleness monitor thread - @sleeper = Sleeper.new - @staleness_monitor_thread = Thread.new do + @json_drb.method_whitelist = @api_whitelist begin - stale = [] - prev_stale = [] - while true - # The check_stale method drives System.telemetry to iterate through - # the packets and mark them stale as necessary. - System.telemetry.check_stale - - # Get all stale packets that include limits items. - stale_pkts = System.telemetry.stale(true) - - # Send :STALE_PACKET events for all newly stale packets. - stale = [] - stale_pkts.each do |packet| - pkt_name = [packet.target_name, packet.packet_name] - stale << pkt_name - post_limits_event(:STALE_PACKET, pkt_name) unless prev_stale.include?(pkt_name) - end - - # Send :STALE_PACKET_RCVD events for all packets that were stale - # but are no longer stale. - prev_stale.each do |pkt_name| - post_limits_event(:STALE_PACKET_RCVD, pkt_name) unless stale.include?(pkt_name) - end - prev_stale = stale.dup - - broken = @sleeper.sleep(10) - break if broken + if @mode == :CMD_TLM_SERVER + @json_drb.start_service(System.listen_hosts['CTS_API'], System.ports['CTS_API'], self) + else + @json_drb.start_service(System.listen_hosts['REPLAY_API'], System.ports['REPLAY_API'], self) + end + rescue Exception + # Call packet_logging shutdown here to explicitly kill the logging + # threads since this CTS is not going to launch + @packet_logging.shutdown + if @mode == :CMD_TLM_SERVER + raise FatalError.new("Error starting JsonDRb on port #{System.ports['CTS_API']}.\nPerhaps a Command and Telemetry Server is already running?") + else + raise FatalError.new("Error starting JsonDRb on port #{System.ports['REPLAY_API']}.\nPerhaps another Replay is already running?") end - rescue Exception => err - Logger.fatal "Staleness Monitor thread unexpectedly died" - Cosmos.handle_fatal_exception(err) end - end # end Thread.new - end + + if @mode == :CMD_TLM_SERVER + @routers.add_preidentified('PREIDENTIFIED_ROUTER', System.ports['CTS_PREIDENTIFIED']) + @routers.add_cmd_preidentified('PREIDENTIFIED_CMD_ROUTER', System.ports['CTS_CMD_ROUTER']) + else + @routers.all.clear + @routers.add_preidentified('PREIDENTIFIED_ROUTER', System.ports['REPLAY_PREIDENTIFIED']) + @routers.add_cmd_preidentified('PREIDENTIFIED_CMD_ROUTER', System.ports['REPLAY_CMD_ROUTER']) + end + System.telemetry.limits_change_callback = method(:limits_change_callback) + @routers.start + + start(production) + end + end # end def initialize # Properly shuts down the command and telemetry server by stoping the # JSON-RPC server, background tasks, routers, and interfaces. Also kills - # the packet staleness monitor thread. + # the packet staleness monitor thread. This is final and the server cannot be + # restarted, it must be recreated def stop # Shutdown DRb - @json_drb.stop_service + @json_drb.stop_service if @json_drb + @routers.stop - # Shutdown staleness monitor thread - Cosmos.kill_thread(self, @staleness_monitor_thread) + if @mode == :CMD_TLM_SERVER + # Shutdown staleness monitor thread + Cosmos.kill_thread(self, @staleness_monitor_thread) - @background_tasks.stop_all - @routers.stop - @interfaces.stop - @packet_logging.shutdown + @background_tasks.stop_all + @interfaces.stop + @packet_logging.shutdown + else + @replay_backend.shutdown + end @stop_callback.call if @stop_callback @message_log.stop if @message_log - - @json_drb = nil end # Set a stop callback @@ -273,6 +256,21 @@ def stop_callback= (stop_callback) @stop_callback = stop_callback end + # Reload the default configuration + def reload + @replay_backend.shutdown if @mode != :CMD_TLM_SERVER + if @reload_callback + @reload_callback.call(false) + else + System.reset + end + end + + # Set a reload callback + def reload_callback= (reload_callback) + @reload_callback = reload_callback + end + # Gracefully kill the staleness monitor thread def graceful_kill @sleeper.cancel @@ -306,13 +304,15 @@ def limits_change_callback(packet, item, old_limits_state, value, log_change) post_limits_event(:LIMITS_CHANGE, [packet.target_name, packet.packet_name, item.name, old_limits_state, item.limits.state]) - if item.limits.response - begin - item.limits.response.call(packet, item, old_limits_state) - rescue Exception => err - Logger.error "#{packet.target_name} #{packet.packet_name} #{item.name} Limits Response Exception!" - Logger.error "Called with old_state = #{old_limits_state}, new_state = #{item.limits.state}" - Logger.error err.formatted + if @mode == :CMD_TLM_SERVER + if item.limits.response + begin + item.limits.response.call(packet, item, old_limits_state) + rescue Exception => err + Logger.error "#{packet.target_name} #{packet.packet_name} #{item.name} Limits Response Exception!" + Logger.error "Called with old_state = #{old_limits_state}, new_state = #{item.limits.state}" + Logger.error err.formatted + end end end end @@ -482,16 +482,22 @@ def self.subscribe_packet_data(packets, upcase_packets = [] # Upper case packet names + need_meta = false packets.length.times do |index| upcase_packets << [] upcase_packets[index][0] = packets[index][0].upcase upcase_packets[index][1] = packets[index][1].upcase + # Get the packet to ensure it exists if @@instance.disconnect @last_subscribed_packet = System.telemetry.packet(upcase_packets[index][0], upcase_packets[index][1]) else @@instance.get_tlm_packet(upcase_packets[index][0], upcase_packets[index][1]) end + + if upcase_packets[index][0] == 'SYSTEM' and upcase_packets[index][1] == 'META' + need_meta = true + end end @@instance.packet_data_queue_mutex.synchronize do @@ -499,6 +505,15 @@ def self.subscribe_packet_data(packets, @@instance.packet_data_queues[id] = [Queue.new, upcase_packets, queue_size] @@instance.next_packet_data_queue_id += 1 + + # Send the current meta packet first if requested + if need_meta + packet = System.telemetry.packet('SYSTEM', 'META') + received_time = packet.received_time + received_time ||= Time.now.sys + @@instance.packet_data_queues[id][0] << [packet.buffer, 'SYSTEM', 'META', + received_time.tv_sec, received_time.tv_usec, packet.received_count] + end end return id end @@ -644,7 +659,7 @@ def self.get_server_message(id, non_block = false) # request_count on json_drb to 0. def self.clear_counters System.clear_counters - self.instance.interfaces.clear_counters + self.instance.interfaces.clear_counters if self.instance.interfaces self.instance.routers.clear_counters self.instance.json_drb.request_count = 0 end @@ -658,5 +673,67 @@ def identified_packet_callback(packet) packet.check_limits(System.limits_set) post_packet(packet) end + + private + + # Start up the system by starting the JSON-RPC server, interfaces, routers, + # and background tasks. Starts a thread to monitor all packets for + # staleness so other tools (such as Packet Viewer or Telemetry Viewer) can + # react accordingly. + # + # This method is shoudl only called by initialize which is why it is private + # + # @param start_packet_logging [Boolean] Whether to start logging data or not + def start(start_packet_logging = false) + if @mode == :CMD_TLM_SERVER + @replay_backend = nil # Remove access to Replay + @message_log = MessageLog.new('server') + @packet_logging.start if start_packet_logging + @interfaces.start + @background_tasks.start_all + + # Start staleness monitor thread + @sleeper = Sleeper.new + @staleness_monitor_thread = Thread.new do + begin + stale = [] + prev_stale = [] + while true + # The check_stale method drives System.telemetry to iterate through + # the packets and mark them stale as necessary. + System.telemetry.check_stale + + # Get all stale packets that include limits items. + stale_pkts = System.telemetry.stale(true) + + # Send :STALE_PACKET events for all newly stale packets. + stale = [] + stale_pkts.each do |packet| + pkt_name = [packet.target_name, packet.packet_name] + stale << pkt_name + post_limits_event(:STALE_PACKET, pkt_name) unless prev_stale.include?(pkt_name) + end + + # Send :STALE_PACKET_RCVD events for all packets that were stale + # but are no longer stale. + prev_stale.each do |pkt_name| + post_limits_event(:STALE_PACKET_RCVD, pkt_name) unless stale.include?(pkt_name) + end + prev_stale = stale.dup + + broken = @sleeper.sleep(10) + break if broken + end + rescue Exception => err + Logger.fatal "Staleness Monitor thread unexpectedly died" + Cosmos.handle_fatal_exception(err) + end + end # end Thread.new + else + # Prevent access to interfaces or packet_logging + @interfaces = nil + @packet_logging = nil + end + end end end diff --git a/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_gui.rb b/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_gui.rb index 224ca290d..55cc7b023 100644 --- a/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_gui.rb +++ b/lib/cosmos/tools/cmd_tlm_server/cmd_tlm_server_gui.rb @@ -1,6 +1,6 @@ # encoding: ascii-8bit -# Copyright 2014 Ball Aerospace & Technologies Corp. +# Copyright 2017 Ball Aerospace & Technologies Corp. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -16,6 +16,7 @@ require 'cosmos/tools/cmd_tlm_server/gui/packets_tab' require 'cosmos/tools/cmd_tlm_server/gui/logging_tab' require 'cosmos/tools/cmd_tlm_server/gui/status_tab' + require 'cosmos/tools/cmd_tlm_server/gui/replay_tab' require 'cosmos/gui/qt_tool' require 'cosmos/gui/dialogs/splash' require 'cosmos/gui/dialogs/exception_dialog' @@ -110,69 +111,93 @@ def meta_callback def initialize(options) super(options) # MUST BE FIRST - All code before super is executed twice in RubyQt Based classes - Cosmos.load_cosmos_icon("cts.png") + @ready = false + @tabs_ready = false + if options.replay + @mode = :REPLAY + Cosmos.load_cosmos_icon("replay.png") + else + @mode = :CMD_TLM_SERVER + Cosmos.load_cosmos_icon("cts.png") + end @production = options.production @no_prompt = options.no_prompt @message_log = nil @output_sleeper = Sleeper.new @first_output = 0 - @interfaces_tab = InterfacesTab.new(self) - @targets_tab = TargetsTab.new - @packets_tab = PacketsTab.new(self) - @logging_tab = LoggingTab.new(@production) - @status_tab = StatusTab.new + @options = options statusBar.showMessage(tr("")) # Show blank message to initialize status bar initialize_actions() initialize_menus() initialize_central_widget() - configure_tabs(options) - complete_initialize() - end - - def configure_tabs(options) Splash.execute(self) do |splash| ConfigParser.splash = splash - splash.message = "Initializing #{TOOL_NAME}" + process_server_messages(@options) + start(splash) + ConfigParser.splash = nil + end + complete_initialize() + end - # Start the thread that will process server messages and add them to the output text - process_server_messages(options) + def start(splash) + splash.message = "Initializing #{@options.title}" if splash + if !CmdTlmServer.instance or @mode == :CMD_TLM_SERVER CmdTlmServer.meta_callback = method(:meta_callback) - cts = CmdTlmServer.new(options.config_file, @production) + cts = CmdTlmServer.new(@options.config_file, @production, false, @mode) cts.stop_callback = method(:stop_callback) + cts.reload_callback = method(:reload) + CmdTlmServer.replay_backend.config_change_callback = method(:config_change_callback) if @mode != :CMD_TLM_SERVER @message_log = CmdTlmServer.message_log + @ready = true + end - # Now that we've started the server (CmdTlmServer.new) we can populate all the tabs - splash.message = "Populating Tabs" - Qt.execute_in_main_thread(true) do - # Override the default title if one was given in the config file - self.window_title = CmdTlmServer.title if CmdTlmServer.title - splash.progress = 0 - @interfaces_tab.populate_interfaces(@tab_widget) - splash.progress = 100/7 * 1 - @targets_tab.populate(@tab_widget) - splash.progress = 100/7 * 2 - @packets_tab.populate_commands(@tab_widget) - splash.progress = 100/7 * 3 - @packets_tab.populate_telemetry(@tab_widget) - splash.progress = 100/7 * 4 - @interfaces_tab.populate_routers(@tab_widget) - splash.progress = 100/7 * 5 - @logging_tab.populate(@tab_widget) - splash.progress = 100/7 * 6 - @status_tab.populate(@tab_widget) - splash.progress = 100 + # Now that we've started the server (CmdTlmServer.new) we can populate all the tabs + splash.message = "Populating Tabs" if splash + Qt.execute_in_main_thread(true) do + # Override the default title if one was given in the config file + self.window_title = CmdTlmServer.title if CmdTlmServer.title + splash.progress = 0 if splash + @tabs_ready = false + if @mode == :CMD_TLM_SERVER + @interfaces_tab.populate_interfaces + else + @replay_tab.populate end - ConfigParser.splash = nil + splash.progress = 100/7 * 1 if splash + @targets_tab.populate + splash.progress = 100/7 * 2 if splash + @commands_tab.populate_commands + splash.progress = 100/7 * 3 if splash + @telemetry_tab.populate_telemetry + splash.progress = 100/7 * 4 if splash + @routers_tab.populate_routers + if @mode == :CMD_TLM_SERVER + splash.progress = 100/7 * 5 if splash + @logging_tab.populate + end + splash.progress = 100/7 * 6 if splash + @status_tab.populate + splash.progress = 100 if splash + @tabs_ready = true + @tab_widget.setCurrentIndex(0) + handle_tab_change(0) end end def initialize_actions super() + # File actions + @file_reload = Qt::Action.new(tr('&Reload Configuration'), self) + @file_reload.statusTip = tr('Reload configuraton and reset') + @file_reload.connect(SIGNAL('triggered()')) do + CmdTlmServer.instance.reload() + end + # Edit actions @edit_clear_counters = Qt::Action.new(tr('&Clear Counters'), self) @edit_clear_counters.statusTip = tr('Clear counters for all interfaces and targets') @@ -181,6 +206,7 @@ def initialize_actions def initialize_menus @file_menu = menuBar.addMenu(tr('&File')) + @file_menu.addAction(@file_reload) @file_menu.addAction(@exit_action) # Do not allow clear counters in production mode @@ -189,8 +215,12 @@ def initialize_menus @edit_menu.addAction(@edit_clear_counters) end - @about_string = "#{TOOL_NAME} is the heart of the COSMOS system. " - @about_string << "It connects to the target and processes command and telemetry requests from other tools." + if @mode == :CMD_TLM_SERVER + @about_string = "#{TOOL_NAME} is the heart of the COSMOS system. " + @about_string << "It connects to the target and processes command and telemetry requests from other tools." + else + @about_string = "Replay allows playing back data into the COSMOS realtime tools. " + end initialize_help_menu() end @@ -222,30 +252,96 @@ def initialize_central_widget Logger.level = Logger::INFO @tab_thread = nil + + if @mode == :CMD_TLM_SERVER + @interfaces_tab = InterfacesTab.new(self, InterfacesTab::INTERFACES, @tab_widget) + else + @replay_tab = ReplayTab.new(@tab_widget) + end + @targets_tab = TargetsTab.new(@tab_widget) + @commands_tab = PacketsTab.new(self, PacketsTab::COMMANDS, @tab_widget) + @telemetry_tab = PacketsTab.new(self, PacketsTab::TELEMETRY, @tab_widget) + @routers_tab = InterfacesTab.new(self, InterfacesTab::ROUTERS, @tab_widget) + if @mode == :CMD_TLM_SERVER + @logging_tab = LoggingTab.new(@production, @tab_widget) + end + @status_tab = StatusTab.new(@tab_widget) + end + + def config_change_callback + start(nil) + end + + def reload(confirm = true) + Qt.execute_in_main_thread(true) do + if confirm + msg = Qt::MessageBox.new(self) + msg.setIcon(Qt::MessageBox::Question) + msg.setText("Are you sure? All connections will temporarily disconnect as the server restarts") + msg.setWindowTitle('Confirm Reload') + msg.setStandardButtons(Qt::MessageBox::Yes | Qt::MessageBox::No) + continue = false + continue = true if msg.exec() == Qt::MessageBox::Yes + msg.dispose + else + continue = true + end + + if continue + Splash.execute(self) do |splash| + ConfigParser.splash = splash + Qt.execute_in_main_thread(true) do + @tab_widget.setCurrentIndex(0) + end + if @mode == :CMD_TLM_SERVER + Qt.execute_in_main_thread(true) do + splash.message = "Stopping Threads" + stop_threads() + end + end + System.reset + start(splash) + Qt.execute_in_main_thread(true) do + @tab_widget.setCurrentIndex(0) + handle_tab_change(0) + end + ConfigParser.splash = nil + end + end + end end # Called when the user changes tabs in the Server application. It kills the # currently executing tab and then creates a new thread to update the GUI # for the selected tab. def handle_tab_change(index) + return unless @tabs_ready kill_tab_thread() @tab_sleeper = Sleeper.new case index when 0 - handle_tab('Interfaces') { @interfaces_tab.update(InterfacesTab::INTERFACES) } + if @mode == :CMD_TLM_SERVER + handle_tab('Interfaces') { @interfaces_tab.update } + else + handle_tab('Replay', 0.5) { @replay_tab.update } + end when 1 handle_tab('Targets') { @targets_tab.update } when 2 - handle_tab('Commands') { @packets_tab.update(PacketsTab::COMMANDS) } + handle_tab('Commands') { @commands_tab.update } when 3 - handle_tab('Telemetry') { @packets_tab.update(PacketsTab::TELEMETRY) } + handle_tab('Telemetry') { @telemetry_tab.update } when 4 - handle_tab('Routers') { @interfaces_tab.update(InterfacesTab::ROUTERS) } + handle_tab('Routers') { @routers_tab.update } when 5 - handle_tab('Logging') { @logging_tab.update } + if @mode == :CMD_TLM_SERVER + handle_tab('Logging') { @logging_tab.update } + else + handle_tab('Status') { @status_tab.update } + end when 6 - handle_status_tab() + handle_tab('Status') { @status_tab.update } end end @@ -253,7 +349,9 @@ def handle_tab_change(index) def kill_tab_thread @tab_sleeper ||= nil @tab_sleeper.cancel if @tab_sleeper + @tab_thread_shutdown = true Qt::CoreApplication.instance.processEvents + Qt::RubyThreadFix.queue.pop.call until Qt::RubyThreadFix.queue.empty? Cosmos.kill_thread(self, @tab_thread) @tab_thread = nil end @@ -263,12 +361,18 @@ def kill_tab_thread # Finally it sleeps using a sleeper so it can be interrupted. # # @param name [String] Name of the tab - def handle_tab(name) + def handle_tab(name, period = 1.0) + @tab_thread_shutdown = false @tab_thread = Thread.new do begin while true + start_time = Time.now.sys + break if @tab_thread_shutdown Qt.execute_in_main_thread(true) { yield } - break if @tab_sleeper.sleep(1) + total_time = Time.now.sys - start_time + if total_time > 0.0 and total_time < period + break if @tab_sleeper.sleep(period - total_time) + end end rescue Exception => error Qt.execute_in_main_thread(true) {|| ExceptionDialog.new(self, error, "COSMOS CTS : #{name} Tab Thread")} @@ -276,22 +380,11 @@ def handle_tab(name) end end - # Update the status tab of the server - def handle_status_tab - @tab_thread = Thread.new do - begin - while true - start_time = Time.now.sys - Qt.execute_in_main_thread(true) { @status_tab.update } - total_time = Time.now.sys - start_time - if total_time > 0.0 and total_time < 1.0 - break if @tab_sleeper.sleep(1.0 - total_time) - end - end - rescue Exception => error - Qt.execute_in_main_thread(true) {|| ExceptionDialog.new(self, error, "COSMOS CTS : Status Tab Thread")} - end - end + def stop_threads + kill_tab_thread() + @replay_tab.shutdown if @replay_tab + CmdTlmServer.instance.stop_logging('ALL') if @mode == :CMD_TLM_SERVER + CmdTlmServer.instance.stop end # Called when the user tries to close the server application. Popup a @@ -304,7 +397,11 @@ def closeEvent(event) else msg = Qt::MessageBox.new(self) msg.setIcon(Qt::MessageBox::Question) - msg.setText("Are you sure? All tools connected to this CmdTlmServer will lose connections and cease to function if the CmdTlmServer is closed.") + if @mode == :CMD_TLM_SERVER + msg.setText("Are you sure? All tools connected to this CmdTlmServer will lose connections and cease to function if the CmdTlmServer is closed.") + else + msg.setText("Are you sure? All tools connected to this Replay will lose connections and cease to function if the Replay is closed.") + end msg.setWindowTitle('Confirm Close') msg.setStandardButtons(Qt::MessageBox::Yes | Qt::MessageBox::No) continue = false @@ -313,9 +410,7 @@ def closeEvent(event) end if continue - kill_tab_thread() - CmdTlmServer.instance.stop_logging('ALL') - CmdTlmServer.instance.stop + stop_threads() super(event) else event.ignore() @@ -328,7 +423,7 @@ def process_server_messages(options) # Start thread to read server messages @output_thread = Thread.new do begin - while !@message_log + while !@ready sleep(1) end while true @@ -360,7 +455,7 @@ def handle_string_output @first_output += 1 end clean_lines, messages = CmdTlmServerGui.process_output_colors(lines_to_write) - @message_log.write(clean_lines) + @message_log.write(clean_lines) if @message_log messages.each {|msg| CmdTlmServer.instance.post_server_message(msg) } end end @@ -369,11 +464,13 @@ def handle_string_output # CmdTlmServer stop callback called by CmdTlmServer.stop. Ensures all the # output is written to the message logs. def stop_callback - handle_string_output() - @output_sleeper.cancel - Qt::CoreApplication.processEvents() - Cosmos.kill_thread(self, @output_thread) - handle_string_output() + Qt.execute_in_main_thread(true) do + handle_string_output() + @output_sleeper.cancel + Qt::CoreApplication.processEvents() + Cosmos.kill_thread(self, @output_thread) + handle_string_output() + end end def graceful_kill @@ -404,6 +501,16 @@ def self.no_gui_stop_callback no_gui_handle_string_output() end + def self.no_gui_reload_callback(confirm = false) + CmdTlmServer.instance.stop_logging('ALL') if @mode == :CMD_TLM_SERVER + CmdTlmServer.instance.stop + System.reset + cts = CmdTlmServer.new(options.config_file, options.production) + @message_log = CmdTlmServer.message_log + cts.stop_callback = method(:no_gui_stop_callback) + cts.reload_callback = method(:no_gui_reload_callback) + end + def self.process_output_colors(lines) clean_lines = '' messages = [] @@ -439,6 +546,7 @@ def self.post_options_parsed_hook(options) Logger.level = Logger::INFO cts = CmdTlmServer.new(options.config_file, options.production) @message_log = CmdTlmServer.message_log + @ready = true @output_thread = Thread.new do while true no_gui_handle_string_output() @@ -446,6 +554,7 @@ def self.post_options_parsed_hook(options) end end cts.stop_callback = method(:no_gui_stop_callback) + cts.reload_callback = method(:no_gui_reload_callback) sleep # Sleep until waked by signal ensure if defined? cts and cts @@ -486,6 +595,13 @@ def self.run(option_parser = nil, options = nil) options.production = false options.no_prompt = false options.no_gui = false + if self.name == "Cosmos::Replay" + options.replay = true + options.title = "Replay" + else + options.replay = false + end + option_parser.separator "CTS Specific Options:" option_parser.on("-c", "--config FILE", "Use the specified configuration file") do |arg| options.config_file = arg diff --git a/lib/cosmos/tools/cmd_tlm_server/gui/interfaces_tab.rb b/lib/cosmos/tools/cmd_tlm_server/gui/interfaces_tab.rb index a5146123c..0be23cfc9 100644 --- a/lib/cosmos/tools/cmd_tlm_server/gui/interfaces_tab.rb +++ b/lib/cosmos/tools/cmd_tlm_server/gui/interfaces_tab.rb @@ -20,38 +20,51 @@ class InterfacesTab ROUTERS = 'Routers' ALIGN_CENTER = Qt::AlignCenter - def initialize(server_gui) + def initialize(server_gui, name, tab_widget) @server_gui = server_gui - @interfaces_table = {} + @name = name + @widget = nil + reset() + @scroll = Qt::ScrollArea.new + @scroll.setMinimumSize(800, 150) + tab_widget.addTab(@scroll, @name) + end + + def reset + Qt.execute_in_main_thread(true) do + @widget.destroy if @widget + @widget = nil + @interfaces_table = nil + end end # Create the interfaces tab and add it to the tab_widget # # @param tab_widget [Qt::TabWidget] The tab widget to add the tab to - def populate_interfaces(tab_widget) - populate(INTERFACES, CmdTlmServer.interfaces, tab_widget) + def populate_interfaces + populate(CmdTlmServer.interfaces) end # Create the routers tab and add it to the tab_widget # # @param tab_widget [Qt::TabWidget] The tab widget to add the tab to - def populate_routers(tab_widget) - populate(ROUTERS, CmdTlmServer.routers, tab_widget) + def populate_routers + populate(CmdTlmServer.routers) end # Update the interfaces or routers tab # # @param name [String] Must be Interfaces or Routers - def update(name) - if name == ROUTERS + def update + if @name == ROUTERS interfaces = CmdTlmServer.routers else interfaces = CmdTlmServer.interfaces end row = 0 interfaces.all.each do |interface_name, interface| - button = @interfaces_table[name].cellWidget(row,1) - state = @interfaces_table[name].item(row,2) + button = @interfaces_table.cellWidget(row,1) + state = @interfaces_table.item(row,2) if interface.connected? and interface.thread button.setText('Disconnect') button.setDisabled(true) if interface.disable_disconnect @@ -73,17 +86,17 @@ def update(name) state.setText('false') state.textColor = Cosmos::BLACK end - @interfaces_table[name].item(row,3).setText(interface.num_clients.to_s) - @interfaces_table[name].item(row,4).setText(interface.write_queue_size.to_s) - @interfaces_table[name].item(row,5).setText(interface.read_queue_size.to_s) - @interfaces_table[name].item(row,6).setText(interface.bytes_written.to_s) - @interfaces_table[name].item(row,7).setText(interface.bytes_read.to_s) - if name == ROUTERS - @interfaces_table[name].item(row,8).setText(interface.read_count.to_s) - @interfaces_table[name].item(row,9).setText(interface.write_count.to_s) + @interfaces_table.item(row,3).setText(interface.num_clients.to_s) + @interfaces_table.item(row,4).setText(interface.write_queue_size.to_s) + @interfaces_table.item(row,5).setText(interface.read_queue_size.to_s) + @interfaces_table.item(row,6).setText(interface.bytes_written.to_s) + @interfaces_table.item(row,7).setText(interface.bytes_read.to_s) + if @name == ROUTERS + @interfaces_table.item(row,8).setText(interface.read_count.to_s) + @interfaces_table.item(row,9).setText(interface.write_count.to_s) else - @interfaces_table[name].item(row,8).setText(interface.write_count.to_s) - @interfaces_table[name].item(row,9).setText(interface.read_count.to_s) + @interfaces_table.item(row,8).setText(interface.write_count.to_s) + @interfaces_table.item(row,9).setText(interface.read_count.to_s) end row += 1 end @@ -91,13 +104,12 @@ def update(name) private - def populate(name, interfaces, tab_widget) + def populate(interfaces) + reset() return if interfaces.all.empty? - scroll = Qt::ScrollArea.new - scroll.setMinimumSize(800, 150) - widget = Qt::Widget.new - layout = Qt::VBoxLayout.new(widget) + @widget = Qt::Widget.new + layout = Qt::VBoxLayout.new(@widget) # Since the layout will be inside a scroll area make sure it respects the sizes we set layout.setSizeConstraint(Qt::Layout::SetMinAndMaxSize) @@ -105,28 +117,27 @@ def populate(name, interfaces, tab_widget) interfaces_table.verticalHeader.hide() interfaces_table.setRowCount(interfaces.all.length) interfaces_table.setColumnCount(11) - if name == ROUTERS + if @name == ROUTERS interfaces_table.setHorizontalHeaderLabels(["Router", "Connect/Disconnect", "Connected?", "Clients", "Tx Q Size", "Rx Q Size", " Bytes Tx ", " Bytes Rx ", " Pkts Rcvd ", " Pkts Sent ", "View Raw"]) else interfaces_table.setHorizontalHeaderLabels(["Interface", "Connect/Disconnect", "Connected?", "Clients", "Tx Q Size", "Rx Q Size", " Bytes Tx ", " Bytes Rx ", " Cmd Pkts ", " Tlm Pkts ", "View Raw"]) end - populate_interface_table(name, interfaces, interfaces_table) + populate_interface_table(interfaces, interfaces_table) interfaces_table.displayFullSize layout.addWidget(interfaces_table) - scroll.setWidget(widget) - @interfaces_table[name] = interfaces_table - tab_widget.addTab(scroll, name) + @scroll.setWidget(@widget) + @interfaces_table = interfaces_table end - def populate_interface_table(name, interfaces, interfaces_table) + def populate_interface_table(interfaces, interfaces_table) row = 0 interfaces.all.each do |interface_name, interface| item = Qt::TableWidgetItem.new(Qt::Object.tr(interface_name)) item.setTextAlignment(ALIGN_CENTER) interfaces_table.setItem(row, 0, item) - interfaces_table.setCellWidget(row, 1, create_button(name, interface, interface_name)) + interfaces_table.setCellWidget(row, 1, create_button(interface, interface_name)) interfaces_table.setItem(row, 2, create_state(interface)) index = 3 @@ -142,7 +153,7 @@ def populate_interface_table(name, interfaces, interfaces_table) view_raw = Qt::PushButton.new("View Raw") view_raw.connect(SIGNAL('clicked()')) do @raw_dialogs ||= [] - if name == ROUTERS + if @name == ROUTERS current_interface = CmdTlmServer.routers.all[interface_name] else current_interface = CmdTlmServer.interfaces.all[interface_name] @@ -154,7 +165,7 @@ def populate_interface_table(name, interfaces, interfaces_table) end end - def create_button(name, interface, interface_name) + def create_button(interface, interface_name) if interface.connected? and interface.thread button_text = 'Disconnect' elsif interface.thread @@ -165,7 +176,7 @@ def create_button(name, interface, interface_name) button_text = 'Connect' end button = Qt::PushButton.new(button_text) - if name == ROUTERS + if @name == ROUTERS button.connect(SIGNAL('clicked()')) do if CmdTlmServer.routers.all[interface_name].thread Logger.info "User disconnecting router #{interface_name}" diff --git a/lib/cosmos/tools/cmd_tlm_server/gui/logging_tab.rb b/lib/cosmos/tools/cmd_tlm_server/gui/logging_tab.rb index 6d042a118..bcef2be33 100644 --- a/lib/cosmos/tools/cmd_tlm_server/gui/logging_tab.rb +++ b/lib/cosmos/tools/cmd_tlm_server/gui/logging_tab.rb @@ -16,18 +16,29 @@ module Cosmos # Implements the logging tab in the Command and Telemetry Server GUI class LoggingTab - def initialize(production) + def initialize(production, tab_widget) @production = production - @logging_layouts = {} + @widget = nil + reset() + @scroll = Qt::ScrollArea.new + tab_widget.addTab(@scroll, "Logging") + end + + def reset + Qt.execute_in_main_thread(true) do + @widget.destroy if @widget + @widget = nil + @logging_layouts = {} + end end # Create the logging tab and add it to the tab_widget # # @param tab_widget [Qt::TabWidget] The tab widget to add the tab to - def populate(tab_widget) - scroll = Qt::ScrollArea.new - widget = Qt::Widget.new - layout = Qt::VBoxLayout.new(widget) + def populate + reset() + @widget = Qt::Widget.new + layout = Qt::VBoxLayout.new(@widget) # Since the layout will be inside a scroll area # make sure it respects the sizes we set layout.setSizeConstraint(Qt::Layout::SetMinAndMaxSize) @@ -38,8 +49,7 @@ def populate(tab_widget) populate_log_file_info(layout) # Set the scroll area widget last now that all the items have been layed out - scroll.setWidget(widget) - tab_widget.addTab(scroll, "Logging") + @scroll.setWidget(@widget) end def update diff --git a/lib/cosmos/tools/cmd_tlm_server/gui/packets_tab.rb b/lib/cosmos/tools/cmd_tlm_server/gui/packets_tab.rb index 9ab55a677..895e3efab 100644 --- a/lib/cosmos/tools/cmd_tlm_server/gui/packets_tab.rb +++ b/lib/cosmos/tools/cmd_tlm_server/gui/packets_tab.rb @@ -19,23 +19,37 @@ class PacketsTab COMMANDS = "Commands" TELEMETRY = "Telemetry" - def initialize(server_gui) + def initialize(server_gui, name, tab_widget) @server_gui = server_gui - @packets_table = {} + @name = name + @widget = nil + reset() + @scroll = Qt::ScrollArea.new + tab_name = "Cmd Packets" if name == COMMANDS + tab_name = "Tlm Packets" if name == TELEMETRY + tab_widget.addTab(@scroll, tab_name) end - def populate_commands(tab_widget) - populate(COMMANDS, System.commands, tab_widget) + def reset + Qt.execute_in_main_thread(true) do + @widget.destroy if @widget + @widget = nil + @packets_table = nil + end end - def populate_telemetry(tab_widget) - populate(TELEMETRY, System.telemetry, tab_widget) + def populate_commands + populate(System.commands) end - def update(name) + def populate_telemetry + populate(System.telemetry) + end + + def update cmd_tlm = nil - cmd_tlm = System.commands if name == COMMANDS - cmd_tlm = System.telemetry if name == TELEMETRY + cmd_tlm = System.commands if @name == COMMANDS + cmd_tlm = System.telemetry if @name == TELEMETRY return if cmd_tlm.nil? || cmd_tlm.target_names.empty? row = 0 @@ -43,18 +57,19 @@ def update(name) packets = cmd_tlm.packets(target_name) packets.sort.each do |packet_name, packet| next if packet.hidden - @packets_table[name].item(row, 2).setText(packet.received_count.to_s) + @packets_table.item(row, 2).setText(packet.received_count.to_s) row += 1 end end packet = cmd_tlm.packet('UNKNOWN', 'UNKNOWN') - @packets_table[name].item(row, 2).setText(packet.received_count.to_s) + @packets_table.item(row, 2).setText(packet.received_count.to_s) row += 1 end private - def populate(name, cmd_tlm, tab_widget) + def populate(cmd_tlm) + reset() return if cmd_tlm.target_names.empty? count = 0 @@ -66,9 +81,8 @@ def populate(name, cmd_tlm, tab_widget) end count += 1 # For UNKNOWN UNKNOWN - scroll = Qt::ScrollArea.new - widget = Qt::Widget.new - layout = Qt::VBoxLayout.new(widget) + @widget = Qt::Widget.new + layout = Qt::VBoxLayout.new(@widget) # Since the layout will be inside a scroll area # make sure it respects the sizes we set layout.setSizeConstraint(Qt::Layout::SetMinAndMaxSize) @@ -81,23 +95,20 @@ def populate(name, cmd_tlm, tab_widget) # Force the last section to fill all available space in the frame #~ table.horizontalHeader.setStretchLastSection(true) headers = ["Target Name", "Packet Name", "Packet Count", "View Raw"] - headers << "View in Command Sender" if name == COMMANDS - headers << "View in Packet Viewer" if name == TELEMETRY + headers << "View in Command Sender" if @name == COMMANDS + headers << "View in Packet Viewer" if @name == TELEMETRY table.setHorizontalHeaderLabels(headers) - populate_packets_table(name, cmd_tlm, table) + populate_packets_table(cmd_tlm, table) table.displayFullSize layout.addWidget(table) - scroll.setWidget(widget) - tab_name = "Cmd Packets" if name == COMMANDS - tab_name = "Tlm Packets" if name == TELEMETRY - tab_widget.addTab(scroll, tab_name) + @scroll.setWidget(@widget) - @packets_table[name] = table + @packets_table = table end - def populate_packets_table(name, cmd_tlm, table) + def populate_packets_table(cmd_tlm, table) row = 0 target_names = cmd_tlm.target_names target_names << 'UNKNOWN'.freeze @@ -116,14 +127,14 @@ def populate_packets_table(name, cmd_tlm, table) view_raw = Qt::PushButton.new("View Raw") view_raw.connect(SIGNAL('clicked()')) do @raw_dialogs ||= [] - @raw_dialogs << CmdRawDialog.new(@server_gui, target_name, packet_name) if name == COMMANDS - @raw_dialogs << TlmRawDialog.new(@server_gui, target_name, packet_name) if name == TELEMETRY + @raw_dialogs << CmdRawDialog.new(@server_gui, target_name, packet_name) if @name == COMMANDS + @raw_dialogs << TlmRawDialog.new(@server_gui, target_name, packet_name) if @name == TELEMETRY end table.setCellWidget(row, 3, view_raw) - if name == COMMANDS + if @name == COMMANDS add_tool_button(table, row, target_name, packet_name, "Command Sender") - elsif name == TELEMETRY + elsif @name == TELEMETRY add_tool_button(table, row, target_name, packet_name, "Packet Viewer") end diff --git a/lib/cosmos/tools/cmd_tlm_server/gui/replay_tab.rb b/lib/cosmos/tools/cmd_tlm_server/gui/replay_tab.rb new file mode 100644 index 000000000..f96e00d91 --- /dev/null +++ b/lib/cosmos/tools/cmd_tlm_server/gui/replay_tab.rb @@ -0,0 +1,242 @@ +# 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' +require 'cosmos/gui/qt' +require 'cosmos/gui/choosers/string_chooser' +require 'cosmos/gui/dialogs/packet_log_dialog' +require 'cosmos/gui/dialogs/progress_dialog' + +module Cosmos + + # Implements the replay tab in the Command and Telemetry Server GUI + class ReplayTab + + attr_accessor :widget + + def initialize(tab_widget) + @widget = nil + @update_slider = true + @file_max_index = 0 + reset() + @scroll = Qt::ScrollArea.new + tab_widget.addTab(@scroll, "Replay") + end + + def reset + CmdTlmServer.replay_backend.reset if CmdTlmServer.instance + Qt.execute_in_main_thread(true) do + @widget.destroy if @widget + @widget = nil + end + end + + # Create the targets tab and add it to the tab_widget + # + # @param tab_widget [Qt::TabWidget] The tab widget to add the tab to + def populate + return if @widget + + @widget = Qt::Widget.new + + layout = Qt::VBoxLayout.new(@widget) + # Since the layout will be inside a scroll area make sure it respects the sizes we set + layout.setSizeConstraint(Qt::Layout::SetMinAndMaxSize) + + @log_widget = Qt::Widget.new + @log_widget.setSizePolicy(Qt::SizePolicy::MinimumExpanding, Qt::SizePolicy::MinimumExpanding) + @log_layout = Qt::VBoxLayout.new() + + # This widget goes inside the top layout so we want 0 contents margins + @log_layout.setContentsMargins(0,0,0,0) + @log_widget.setLayout(@log_layout) + + # Create the log file GUI + @log_file_selection = Qt::GroupBox.new("Log File Selection") + @log_select = Qt::HBoxLayout.new(@log_file_selection) + @log_name = Qt::LineEdit.new + @log_name.setReadOnly(true) + @log_select.addWidget(@log_name) + @log_open = Qt::PushButton.new("Browse...") + @log_select.addWidget(@log_open) + @log_layout.addWidget(@log_file_selection) + + @log_open.connect(SIGNAL('clicked()')) { select_log_file() } + + # Create the operation buttons GUI + @op = Qt::GroupBox.new("Playback Control") + @op_layout = Qt::VBoxLayout.new(@op) + @op_button_layout = Qt::HBoxLayout.new + @move_start = Qt::PushButton.new(Cosmos.get_icon('skip_to_start-26.png'), '') + @move_start.connect(SIGNAL('clicked()')) { CmdTlmServer.replay_backend.move_start() } + @op_button_layout.addWidget(@move_start) + @step_back = Qt::PushButton.new(Cosmos.get_icon('rewind-26.png'), '') + @step_back_timer = Qt::Timer.new + @step_back_timeout = 100 + @step_back_timer.connect(SIGNAL('timeout()')) { CmdTlmServer.replay_backend.step_back(); @step_back_timeout = (@step_back_timeout / 2).to_i; @step_back_timer.start(@step_back_timeout) } + @step_back.connect(SIGNAL('pressed()')) { CmdTlmServer.replay_backend.step_back(); @step_back_timeout = 300; @step_back_timer.start(@step_back_timeout) } + @step_back.connect(SIGNAL('released()')) { @step_back_timer.stop } + @op_button_layout.addWidget(@step_back) + @reverse_play = Qt::PushButton.new(Cosmos.get_icon('reverse-play-26.png'), '') + @reverse_play.connect(SIGNAL('clicked()')) { CmdTlmServer.replay_backend.reverse_play() } + @op_button_layout.addWidget(@reverse_play) + @stop = Qt::PushButton.new(Cosmos.get_icon('stop-26.png'), '') + @stop.connect(SIGNAL('clicked()')) { CmdTlmServer.replay_backend.stop() } + @op_button_layout.addWidget(@stop) + @play = Qt::PushButton.new(Cosmos.get_icon('play-26.png'), '') + @play.connect(SIGNAL('clicked()')) { CmdTlmServer.replay_backend.play() } + @op_button_layout.addWidget(@play) + @step_forward = Qt::PushButton.new(Cosmos.get_icon('fast_forward-26.png'), '') + @step_forward_timer = Qt::Timer.new + @step_forward_timeout = 100 + @step_forward_timer.connect(SIGNAL('timeout()')) { CmdTlmServer.replay_backend.step_forward(); @step_forward_timeout = (@step_forward_timeout / 2).to_i; @step_forward_timer.start(@step_forward_timeout) } + @step_forward.connect(SIGNAL('pressed()')) { CmdTlmServer.replay_backend.step_forward(); @step_forward_timeout = 300; @step_forward_timer.start(@step_forward_timeout) } + @step_forward.connect(SIGNAL('released()')) { @step_forward_timer.stop } + @op_button_layout.addWidget(@step_forward) + @move_end = Qt::PushButton.new(Cosmos.get_icon('end-26.png'), '') + @move_end.connect(SIGNAL('clicked()')) { CmdTlmServer.replay_backend.move_end() } + @op_button_layout.addWidget(@move_end) + @op_layout.addLayout(@op_button_layout) + + # Speed Selection + @playback_delay = nil + @speed_select = Qt::ComboBox.new + @variants = [] + @variants << [Qt::Variant.new(0.0), 0.0] + @speed_select.addItem("No Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(0.001), 0.001] + @speed_select.addItem("1ms Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(0.002), 0.002] + @speed_select.addItem("2ms Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(0.005), 0.005] + @speed_select.addItem("5ms Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(0.01), 0.01] + @speed_select.addItem("10ms Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(0.05), 0.05] + @speed_select.addItem("50ms Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(0.125), 0.125] + @speed_select.addItem("125ms Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(0.25), 0.25] + @speed_select.addItem("250ms Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(0.5), 0.5] + @speed_select.addItem("500ms Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(1.0), 1.0] + @speed_select.addItem("1s Delay", @variants[-1][0]) + @variants << [Qt::Variant.new(nil), nil] + @speed_select.addItem("Realtime", @variants[-1][0]) + @speed_select.setMaxVisibleItems(11) + @speed_select.connect(SIGNAL('currentIndexChanged(int)')) do + CmdTlmServer.replay_backend.set_playback_delay(@speed_select.itemData(@speed_select.currentIndex).value) + end + @speed_layout = Qt::FormLayout.new() + @speed_layout.addRow("&Delay:", @speed_select) + @status = Qt::LineEdit.new + @status.setReadOnly(true) + @status.setText('Stopped') + @speed_layout.addRow("&Status:", @status) + @op_layout.addLayout(@speed_layout) + @log_layout.addWidget(@op) + + @file_pos = Qt::GroupBox.new("File Position") + @file_pos_layout = Qt::VBoxLayout.new(@file_pos) + @slider = Qt::Slider.new(Qt::Horizontal) + @slider.setRange(0, 10000) + @slider.setTickInterval(1000) + @slider.setTickPosition(Qt::Slider::TicksBothSides) + @slider.setTracking(false) + @slider.connect(SIGNAL('sliderPressed()')) { slider_pressed() } + @slider.connect(SIGNAL('sliderReleased()')) { slider_released() } + @time_layout = Qt::HBoxLayout.new() + @start_time = StringChooser.new(@widget, 'Start:', '', 200, true, true, Qt::AlignCenter | Qt::AlignVCenter) + @end_time = StringChooser.new(@widget, 'End:', '', 200, true, true, Qt::AlignCenter | Qt::AlignVCenter) + @current_time = StringChooser.new(@widget, 'Current:', '', 200, true, true, Qt::AlignCenter | Qt::AlignVCenter) + @time_layout.addWidget(@start_time) + @time_layout.addWidget(@current_time) + @time_layout.addWidget(@end_time) + @file_pos_layout.addLayout(@time_layout) + @file_pos_layout.addWidget(@slider) + @log_layout.addWidget(@file_pos) + layout.addWidget(@log_widget) + + @scroll.setWidget(@widget) + end + + # Update the replay tab gui + def update + status, playback_delay, filename, file_start, file_current, file_end, file_index, @file_max_index = CmdTlmServer.replay_backend.status + @status.setText(status) + @log_name.text = filename + found = false + @variants.each_with_index do |data, index| + value = data[1] + if !playback_delay.nil? and !value.nil? + if (playback_delay >= (value - 0.0001)) and (playback_delay <= (value + 0.0001)) + @speed_select.currentIndex = index + found = true + break + end + else + if playback_delay == value + @speed_select.currentIndex = index + found = true + break + end + end + end + unless found + @variants << [Qt::Variant.new(playback_delay.to_f), playback_delay.to_f] + @speed_select.addItem("#{(playback_delay.to_f * 1000.0).to_i}ms Delay", @variants[-1][0]) + @speed_select.currentIndex = @variants.length - 1 + end + @start_time.value = file_start + @current_time.value = file_current + @end_time.value = file_end + + if @update_slider + if @file_max_index != 0 + value = (((file_index - 1).to_f / @file_max_index.to_f) * 10000.0).to_i + else + value = 0 + end + @slider.setSliderPosition(value) + @slider.setValue(value) + end + end + + def slider_pressed + @update_slider = false + CmdTlmServer.replay_backend.stop + end + + def slider_released + CmdTlmServer.replay_backend.move_index(((@slider.sliderPosition / 10000.0) * (@file_max_index - 1)).to_i) + @update_slider = true + end + + def shutdown + CmdTlmServer.replay_backend.shutdown + end + + private + + def select_log_file + packet_log_dialog = PacketLogDialog.new( + @widget, 'Select Log File', CmdTlmServer.replay_backend.log_directory, CmdTlmServer.replay_backend.packet_log_reader, + [], nil, false, false, true, Cosmos::TLM_FILE_PATTERN, + Cosmos::BIN_FILE_PATTERN, false + ) + case packet_log_dialog.exec + when Qt::Dialog::Accepted + CmdTlmServer.replay_backend.select_file(packet_log_dialog.filenames[0], packet_log_dialog.packet_log_reader) + end + end + + end +end # module Cosmos diff --git a/lib/cosmos/tools/cmd_tlm_server/gui/status_tab.rb b/lib/cosmos/tools/cmd_tlm_server/gui/status_tab.rb index 0622bbb90..2079d6b49 100644 --- a/lib/cosmos/tools/cmd_tlm_server/gui/status_tab.rb +++ b/lib/cosmos/tools/cmd_tlm_server/gui/status_tab.rb @@ -14,21 +14,33 @@ module Cosmos # Implements the status tab in the Command and Telemetry Server GUI class StatusTab + def initialize(tab_widget) + @widget = nil + reset() + @scroll = Qt::ScrollArea.new + tab_widget.addTab(@scroll, "Status") + end + + def reset + Qt.execute_in_main_thread(true) do + @widget.destroy if @widget + @widget = nil + end + end + # Create the status tab and add it to the tab_widget # @param tab_widget [Qt::TabWidget] The tab widget to add the tab to - def populate(tab_widget) - scroll = Qt::ScrollArea.new - widget = Qt::Widget.new - layout = Qt::VBoxLayout.new(widget) + def populate + reset() + @widget = Qt::Widget.new + layout = Qt::VBoxLayout.new(@widget) populate_limits_status(layout) populate_api_status(layout) populate_system_status(layout) populate_background_status(layout) - # Set the scroll area widget last now that all the items have been layed out - scroll.setWidget(widget) - tab_widget.addTab(scroll, "Status") + @scroll.setWidget(@widget) end # Update the status tab in the GUI @@ -85,7 +97,11 @@ def populate_api_status(layout) @api_table.setColumnCount(6) @api_table.setHorizontalHeaderLabels(["Port", "Num Clients", "Requests", "Requests/Sec", "Avg Request Time", "Estimated Utilization"]) - @api_table.setItem(0, 0, Qt::TableWidgetItem.new(Qt::Object.tr(System.ports['CTS_API'].to_s))) + if CmdTlmServer.mode == :CMD_TLM_SERVER + @api_table.setItem(0, 0, Qt::TableWidgetItem.new(Qt::Object.tr(System.ports['CTS_API'].to_s))) + else + @api_table.setItem(0, 0, Qt::TableWidgetItem.new(Qt::Object.tr(System.ports['REPLAY_API'].to_s))) + end item0 = Qt::TableWidgetItem.new(Qt::Object.tr(CmdTlmServer.json_drb.num_clients.to_s)) item0.setTextAlignment(Qt::AlignCenter) @api_table.setItem(0, 1, item0) diff --git a/lib/cosmos/tools/cmd_tlm_server/gui/targets_tab.rb b/lib/cosmos/tools/cmd_tlm_server/gui/targets_tab.rb index cc9f274df..af059f07a 100644 --- a/lib/cosmos/tools/cmd_tlm_server/gui/targets_tab.rb +++ b/lib/cosmos/tools/cmd_tlm_server/gui/targets_tab.rb @@ -15,16 +15,29 @@ module Cosmos # Implements the targets tab in the Command and Telemetry Server GUI class TargetsTab + def initialize(tab_widget) + @widget = nil + reset() + @scroll = Qt::ScrollArea.new + tab_widget.addTab(@scroll, "Targets") + end + + def reset + Qt.execute_in_main_thread(true) do + @widget.destroy if @widget + @widget = nil + end + end # Create the targets tab and add it to the tab_widget # # @param tab_widget [Qt::TabWidget] The tab widget to add the tab to - def populate(tab_widget) + def populate + reset() num_targets = System.targets.length if num_targets > 0 - scroll = Qt::ScrollArea.new - widget = Qt::Widget.new - layout = Qt::VBoxLayout.new(widget) + @widget = Qt::Widget.new + layout = Qt::VBoxLayout.new(@widget) # Since the layout will be inside a scroll area make sure it respects the sizes we set layout.setSizeConstraint(Qt::Layout::SetMinAndMaxSize) @@ -39,8 +52,7 @@ def populate(tab_widget) @targets_table.displayFullSize layout.addWidget(@targets_table) - scroll.setWidget(widget) - tab_widget.addTab(scroll, "Targets") + @scroll.setWidget(@widget) end end diff --git a/lib/cosmos/tools/cmd_tlm_server/replay_backend.rb b/lib/cosmos/tools/cmd_tlm_server/replay_backend.rb new file mode 100644 index 000000000..c9109f046 --- /dev/null +++ b/lib/cosmos/tools/cmd_tlm_server/replay_backend.rb @@ -0,0 +1,375 @@ +# encoding: ascii-8bit + +# Copyright 2017 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 + +module Cosmos + + # Handles logic for the Replay mode + class ReplayBackend + + # The number of bytes to print when an UNKNOWN packet is received + UNKNOWN_BYTES_TO_PRINT = 36 + + attr_accessor :log_directory + attr_accessor :log_filename + attr_accessor :packet_log_reader + attr_accessor :config_change_callback + + # @param cmd_tlm_server_config [CmdTlmServerConfig] + def initialize(cmd_tlm_server_config) + @config = cmd_tlm_server_config + reset() + @config_change_callback = nil + end + + # Reset internal state + def reset + @cancel = false + @playback_delay = 0.0 + @default_packet_log_reader = System.default_packet_log_reader.new(*System.default_packet_log_reader_params) + @packet_log_reader = @default_packet_log_reader + @log_directory = System.paths['LOGS'] + @log_directory << '/' unless @log_directory[-1..-1] == '\\' or @log_directory[-1..-1] == '/' + @log_filename = nil + @playing = false + @playback_sleeper = nil + @thread = nil + @playback_index = 0 + @playback_max_index = 0 + @packet_offsets = [] + @progress = 0 + @status = '' + @start_time = '' + @current_time = '' + @end_time = '' + end + + # Select and start analyzing a file for replay + # + # filename [String] filename relative to output logs folder or absolute filename + def select_file(filename, packet_log_reader = 'DEFAULT') + stop() + Cosmos.kill_thread(self, @thread) + @thread = Thread.new do + begin + stop() + if String === packet_log_reader + if packet_log_reader == 'DEFAULT' + @packet_log_reader = @default_packet_log_reader + else + packet_log_reader = Cosmos.require_class(packet_log_reader) + @packet_log_reader = packet_log_reader_class.new + end + elsif !packet_log_reader.nil? + # Instantiated object + @packet_log_reader = packet_log_reader + end # Else use existing + + @log_filename = filename + @log_directory = File.dirname(@log_filename) + @log_directory << '/' unless @log_directory[-1..-1] == '\\' + + System.telemetry.reset + @cancel = false + @progress = 0 + @status = "Analyzing: #{@progress}%" + start_config_name = System.configuration_name + config_change_success, config_error = Cosmos.check_log_configuration(@packet_log_reader, @log_filename) + if System.configuration_name != start_config_name + @config_change_callback.call() if @config_change_callback + end + @packet_offsets = @packet_log_reader.packet_offsets(@log_filename, lambda {|percentage| + progress_int = (percentage * 100).to_i + if @progress != progress_int + @progress = progress_int + @status = "Analyzing: #{@progress}%" + end + @cancel + }) + @playback_index = 0 + @playback_max_index = @packet_offsets.length + @packet_log_reader.open(@log_filename) + + if @cancel + @packet_log_reader.close + @log_filename = '' + @packet_offsets = [] + @playback_index = 0 + @start_time = '' + @current_time = '' + @end_time = '' + else + packet = read_at_index(@packet_offsets.length - 1, :FORWARD) + @end_time = packet.received_time.formatted(true, 3, true) if packet and packet.received_time + packet = read_at_index(0, :FORWARD) + @start_time = packet.received_time.formatted(true, 3, true) if packet and packet.received_time + end + rescue Exception => error + Logger.error "Error in Analysis Thread\n#{error.formatted}" + ensure + @status = 'Stopped' + @playing = false + @playback_sleeper = nil + @thread = nil + end + end + end + + # Get current replay status + # + # @return [status, playback_delay, filename, file_start, file_current, file_end, file_index, file_max_index] + def status + [@status, + @playback_delay, + @log_filename.to_s, + @start_time, + @current_time, + @end_time, + @playback_index, + @playback_max_index] + end + + # Set the replay delay + # + # @param delay [Float] delay between packets in seconds 0.0 to 1.0, nil = No Delay, -1.0 = REALTIME + def set_playback_delay(delay) + if delay + delay = delay.to_f + if delay <= 0.0 + @playback_delay = 0.0 + elsif delay > 1.0 + @playback_delay = 1.0 + else + @playback_delay = delay + end + else + @playback_delay = nil + end + end + + # Replay start playing forward + def play + if @log_filename and !@thread + @playback_index = 1 if @playback_index < 0 + start_playback(:FORWARD) + else + stop() + end + end + + # Replay start playing backward + def reverse_play + if @log_filename and !@thread + @playback_index = @packet_offsets.length - 2 if @playback_index >= @packet_offsets.length + start_playback(:BACKWARD) + else + stop() + end + end + + # Replay stop + def stop + @cancel = true + @playing = false + @playback_sleeper.cancel if @playback_sleeper + end + + # Replay step forward one packet + def step_forward + if @log_filename and !@thread + @playback_index = 1 if @playback_index < 0 + read_at_index(@playback_index, :FORWARD) + else + stop() + end + end + + # Replay step backward one packet + def step_back + if @log_filename and !@thread + @playback_index = @packet_offsets.length - 2 if @playback_index >= @packet_offsets.length + read_at_index(@playback_index, :BACKWARD) + else + stop() + end + end + + # Replay move to start of file + def move_start + if @log_filename and !@thread + packet = read_at_index(0, :FORWARD) + @start_time = packet.received_time.formatted(true, 3, true) if packet and packet.received_time + else + stop() + end + end + + # Replay move to end of file + def move_end + if @log_filename and !@thread + packet = read_at_index(@packet_offsets.length - 1, :FORWARD) + @end_time = packet.received_time.formatted(true, 3, true) if packet and packet.received_time + else + stop() + end + end + + # Replay move to index + # + # @param index [Integer] packet index into file + def move_index(index) + if @log_filename and !@thread + read_at_index(index, :FORWARD) + end + end + + def shutdown + stop() + Cosmos.kill_thread(self, @thread) + reset() + end + + # Gracefully kill threads + def graceful_kill + stop() + end + + private + + def start_playback(direction) + @thread = Thread.new do + @playback_sleeper = Sleeper.new + error = nil + begin + @playing = true + @status = 'Playing' + + previous_packet = nil + while (@playing) + if @playback_delay != 0.0 + packet_start = Time.now.sys + packet = read_at_index(@playback_index, direction) + break unless packet + delay_time = 0.0 + if @playback_delay + # Fixed Time Delay + delay_time = @playback_delay - (Time.now.sys - packet_start) + elsif previous_packet and packet.received_time and previous_packet.received_time + # Realtime + if direction == :FORWARD + delay_time = packet.received_time - previous_packet.received_time - (Time.now.sys - packet_start) + else + delay_time = previous_packet.received_time - packet.received_time - (Time.now.sys - packet_start) + end + end + if delay_time > 0.0 + break if @playback_sleeper.sleep(delay_time) + end + previous_packet = packet + else + # No Delay + packet = read_at_index(@playback_index, direction) + break unless packet + previous_packet = packet + end + end + rescue Exception => error + Logger.error "Error in Playback Thread\n#{error.formatted}" + ensure + @status = 'Stopped' + @playing = false + @playback_sleeper = nil + @thread = nil + end + end + end + + def read_at_index(index, direction) + packet_offset = nil + packet_offset = @packet_offsets[index] if index >= 0 + if packet_offset + # Read the packet + packet = @packet_log_reader.read_at_offset(packet_offset, false) + handle_packet(packet) + + # Adjust index for next read + if direction == :FORWARD + @playback_index = index + 1 + else + @playback_index = index - 1 + end + @current_time = packet.received_time.formatted(true, 3, true) if packet and packet.received_time + + return packet + else + return nil + end + end + + def handle_packet(packet) + # For replay we will try our best here but not crash on errors + begin + interface = nil + + # Identify and update packet + if packet.identified? + # Preidentifed packet - place it into the current value table + identified_packet = System.telemetry.update!(packet.target_name, + packet.packet_name, + packet.buffer) + else + # Packet needs to be identified + identified_packet = System.telemetry.identify!(packet.buffer) + end + + if identified_packet and packet.target_name != 'UNKNOWN' + identified_packet.received_time = packet.received_time + packet = identified_packet + target = System.targets[packet.target_name.upcase] + interface = target.interface if target + else + unknown_packet = System.telemetry.update!('UNKNOWN', 'UNKNOWN', packet.buffer) + unknown_packet.received_time = packet.received_time + packet = unknown_packet + data_length = packet.length + string = "Unknown #{data_length} byte packet starting: " + num_bytes_to_print = [UNKNOWN_BYTES_TO_PRINT, data_length].min + data_to_print = packet.buffer(false)[0..(num_bytes_to_print - 1)] + data_to_print.each_byte do |byte| + string << sprintf("%02X", byte) + end + time_string = '' + time_string = packet.received_time.formatted << ' ' if packet.received_time + puts "#{time_string}ERROR: #{string}" + end + + target = System.targets[packet.target_name] + target.tlm_cnt += 1 if target + packet.received_count += 1 + packet.check_limits(System.limits_set) + CmdTlmServer.instance.post_packet(packet) + + # Write to routers + if interface + interface.routers.each do |router| + begin + router.write(packet) if router.write_allowed? and router.connected? + rescue => err + Logger.error "Problem writing to router #{router.name} - #{err.class}:#{err.message}" + end + end + end + rescue Exception => err + Logger.error "Problem handling packet #{packet.target_name} #{packet.packet_name} - #{err.class}:#{err.message}" + end + end + + end # class ReplayBackend + +end # module Cosmos diff --git a/lib/cosmos/tools/cmd_tlm_server/routers.rb b/lib/cosmos/tools/cmd_tlm_server/routers.rb index 957f7dc61..de2dfb619 100644 --- a/lib/cosmos/tools/cmd_tlm_server/routers.rb +++ b/lib/cosmos/tools/cmd_tlm_server/routers.rb @@ -38,7 +38,11 @@ def add_preidentified(router_name, port) router = TcpipServerInterface.new(port, port, 10.0, nil, 'PREIDENTIFIED') router.name = router_name router.disable_disconnect = true - router.set_option('LISTEN_ADDRESS', [System.listen_hosts['CTS_PREIDENTIFIED']]) + if CmdTlmServer.mode == :CMD_TLM_SERVER + router.set_option('LISTEN_ADDRESS', [System.listen_hosts['CTS_PREIDENTIFIED']]) + else + router.set_option('LISTEN_ADDRESS', [System.listen_hosts['REPLAY_PREIDENTIFIED']]) + end router.set_option('AUTO_SYSTEM_META', [true]) @config.routers[router_name] = router @config.interfaces.each do |interface_name, interface| @@ -58,7 +62,11 @@ def add_cmd_preidentified(cmd_router_name, port) cmd_router = TcpipServerInterface.new(port, nil, 10.0, nil, 'PREIDENTIFIED') cmd_router.name = cmd_router_name cmd_router.disable_disconnect = true - cmd_router.set_option('LISTEN_ADDRESS', [System.listen_hosts['CTS_CMD_ROUTER']]) + if CmdTlmServer.mode == :CMD_TLM_SERVER + cmd_router.set_option('LISTEN_ADDRESS', [System.listen_hosts['CTS_CMD_ROUTER']]) + else + cmd_router.set_option('LISTEN_ADDRESS', [System.listen_hosts['REPLAY_CMD_ROUTER']]) + end cmd_router.set_option('AUTO_SYSTEM_META', [true]) @config.routers[cmd_router_name] = cmd_router @config.interfaces.each do |interface_name, interface| diff --git a/lib/cosmos/tools/data_viewer/data_viewer.rb b/lib/cosmos/tools/data_viewer/data_viewer.rb index 963e718a8..550e14574 100644 --- a/lib/cosmos/tools/data_viewer/data_viewer.rb +++ b/lib/cosmos/tools/data_viewer/data_viewer.rb @@ -103,6 +103,10 @@ def initialize_actions @handle_reset.statusTip = tr('Reset Components') @handle_reset.connect(SIGNAL('triggered()')) { handle_reset() } + @replay_action = Qt::Action.new(tr('Toggle Replay Mode'), self) + @replay_action.statusTip = tr('Toggle Replay Mode') + @replay_action.connect(SIGNAL('triggered()')) { toggle_replay_mode() } + # Search Actions @search_find = Qt::Action.new(Cosmos.get_icon('search.png'), tr('&Find'), self) @search_find_keyseq = Qt::KeySequence.new(tr('Ctrl+F')) @@ -139,6 +143,7 @@ def initialize_menus file_menu = menuBar.addMenu(tr('&File')) file_menu.addAction(@open_log) file_menu.addAction(@handle_reset) + file_menu.addAction(@replay_action) file_menu.addSeparator() file_menu.addAction(@exit_action) @@ -167,6 +172,11 @@ def initialize_central_widget # Create the top level vertical layout @top_layout = Qt::VBoxLayout.new(@central_widget) + @replay_flag = Qt::Label.new("Replay Mode") + @replay_flag.setStyleSheet("background:green;color:white;padding:5px;font-weight:bold;") + @top_layout.addWidget(@replay_flag) + @replay_flag.hide + # Realtime Button Bar @realtime_button_bar = RealtimeButtonBar.new(self) @realtime_button_bar.start_callback = method(:handle_start) @@ -196,6 +206,19 @@ def search_text end end + def toggle_replay_mode + running = (@realtime_button_bar.state == 'Running') + handle_stop() + handle_reset() + set_replay_mode(!get_replay_mode()) + if get_replay_mode() + @replay_flag.show + else + @replay_flag.hide + end + handle_start if running + end + def handle_tab_change(index) # Remove existing actions @tab_menu_actions.each do |action| @@ -246,6 +269,15 @@ def start_subscription_thread @cancel_thread = false @sleeper = Sleeper.new if !@packets.empty? + need_meta = true + @packets.each do |target_name, packet_name| + if target_name == 'SYSTEM' and packet_name == 'META' + need_meta = false + break + end + end + @packets << ['SYSTEM', 'META'] if need_meta + begin while true break if @cancel_thread @@ -281,6 +313,11 @@ def start_subscription_thread packet.received_time = received_time packet.received_count = received_count + # Make sure we are on the right configuration + if target_name == 'SYSTEM' and packet_name == 'META' + System.load_configuration(packet.read('CONFIG')) + end + # Route packet to its component(s) index = packet.target_name + ' ' + packet.packet_name @component_mutex.synchronize do @@ -337,6 +374,8 @@ def handle_reset def handle_start if windowTitle() != 'Data Viewer' + # Switch from log back to realtime/replay + # Clear Title setWindowTitle('Data Viewer') diff --git a/lib/cosmos/tools/limits_monitor/limits_monitor.rb b/lib/cosmos/tools/limits_monitor/limits_monitor.rb index 7aecb9f2f..0e02ff801 100644 --- a/lib/cosmos/tools/limits_monitor/limits_monitor.rb +++ b/lib/cosmos/tools/limits_monitor/limits_monitor.rb @@ -102,7 +102,8 @@ def initialize(new_item_callback, update_item_callback, clear_items_callback, re end # Request that the limits items be refreshed from the server - def request_reset + def request_reset(toggle_mode = false) + @toggle_mode = toggle_mode @initialized = false end @@ -356,6 +357,8 @@ def reset @stale = [] @limits_set = get_limits_set() unsubscribe_limits_events(@queue_id) if @queue_id + set_replay_mode(!get_replay_mode()) if @toggle_mode + @toggle_mode = false @queue_id = subscribe_limits_events(100000) @clear_items_callback.call get_out_of_limits().each do |target, packet, item, state| @@ -549,6 +552,10 @@ def initialize_actions @reset_action.statusTip = tr('Reset connection and clear all items. This does not modify the ignored items.') @reset_action.connect(SIGNAL('triggered()')) { @limits_items.request_reset() } + @replay_action = Qt::Action.new(tr('Toggle Replay Mode'), self) + @replay_action.statusTip = tr('Toggle Replay Mode') + @replay_action.connect(SIGNAL('triggered()')) { toggle_replay_mode() } + @open_ignored_action = Qt::Action.new(Cosmos.get_icon('open.png'), tr('&Open Config'), self) @open_ignored_action_keyseq = Qt::KeySequence.new(tr('Ctrl+O')) @@ -579,6 +586,7 @@ def initialize_menus @file_menu.addSeparator() @file_menu.addAction(@reset_action) @file_menu.addAction(@options_action) + @file_menu.addAction(@replay_action) @file_menu.addSeparator() @file_menu.addAction(@exit_action) @@ -591,8 +599,18 @@ def initialize_menus # Layout the main GUI tab widget with a view of all the out of limits items # in one tab and a log tab showing all limits events. def initialize_central_widget + + widget = Qt::Widget.new + layout = Qt::VBoxLayout.new(widget) + setCentralWidget(widget) + + @replay_flag = Qt::Label.new("Replay Mode") + @replay_flag.setStyleSheet("background:green;color:white;padding:5px;font-weight:bold;") + layout.addWidget(@replay_flag) + @replay_flag.hide + @tabbook = Qt::TabWidget.new(self) - setCentralWidget(@tabbook) + layout.addWidget(@tabbook) @widget = Qt::Widget.new @layout = Qt::VBoxLayout.new(@widget) @@ -677,6 +695,10 @@ def show_options_dialog end end + def toggle_replay_mode + @limits_items.request_reset(true) + end + # @return [String] Fully qualified path to the configuration file def default_config_path # If the config file has been set then just return it @@ -834,7 +856,14 @@ def remove_gui_item(widget) # Reset the GUI by clearing all items def clear_gui_items - Qt.execute_in_main_thread(true) { @scroll_layout.removeAll } + Qt.execute_in_main_thread(true) do + if get_replay_mode() + @replay_flag.show + else + @replay_flag.hide + end + @scroll_layout.removeAll + end end # Update front panel to ignore an item when the corresponding button is pressed. diff --git a/lib/cosmos/tools/packet_viewer/packet_viewer.rb b/lib/cosmos/tools/packet_viewer/packet_viewer.rb index 50f06a410..2dabfd7fe 100644 --- a/lib/cosmos/tools/packet_viewer/packet_viewer.rb +++ b/lib/cosmos/tools/packet_viewer/packet_viewer.rb @@ -102,6 +102,10 @@ def initialize_actions @option_action.statusTip = tr('Application Options') connect(@option_action, SIGNAL('triggered()'), self, SLOT('file_options()')) + @replay_action = Qt::Action.new(tr('Toggle Replay Mode'), self) + @replay_action.statusTip = tr('Toggle Replay Mode') + @replay_action.connect(SIGNAL('triggered()')) { toggle_replay_mode() } + @color_blind_action = Qt::Action.new(tr('Color&blind Mode'), self) @color_blind_keyseq = Qt::KeySequence.new(tr('Ctrl+B')) @color_blind_action.shortcut = @color_blind_keyseq @@ -180,6 +184,7 @@ def initialize_menus file_menu.addAction(@edit_action) file_menu.addAction(@reset_action) file_menu.addAction(@option_action) + file_menu.addAction(@replay_action) file_menu.addSeparator() file_menu.addAction(@exit_action) @@ -207,6 +212,11 @@ def initialize_central_widget(options) # Create the top level vertical layout top_layout = Qt::VBoxLayout.new(central_widget) + @replay_flag = Qt::Label.new("Replay Mode") + @replay_flag.setStyleSheet("background:green;color:white;padding:5px;font-weight:bold;") + top_layout.addWidget(@replay_flag) + @replay_flag.hide + # Set the target combobox selection @target_select = Qt::ComboBox.new @target_select.setMaxVisibleItems(6) @@ -265,6 +275,15 @@ def file_options @polling_rate, 0, 1000, 1, nil) end + def toggle_replay_mode + set_replay_mode(!get_replay_mode()) + if get_replay_mode() + @replay_flag.show + else + @replay_flag.hide + end + end + def edit_definition # Grab all the cmd_tlm_files and processes them in reverse sort order # because typically we'll have cmd.txt and tlm.txt and we want to process diff --git a/lib/cosmos/tools/replay/replay.rb b/lib/cosmos/tools/replay/replay.rb index 42e0b190b..c22baefa3 100644 --- a/lib/cosmos/tools/replay/replay.rb +++ b/lib/cosmos/tools/replay/replay.rb @@ -1,6 +1,6 @@ # encoding: ascii-8bit -# Copyright 2014 Ball Aerospace & Technologies Corp. +# Copyright 2017 Ball Aerospace & Technologies Corp. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -9,509 +9,12 @@ # attribution addendums as found in the LICENSE.txt require 'cosmos' -Cosmos.catch_fatal_exception do - require 'cosmos/gui/qt_tool' - require 'cosmos/gui/dialogs/splash' - require 'cosmos/gui/dialogs/progress_dialog' - require 'cosmos/gui/dialogs/packet_log_dialog' - require 'cosmos/tools/replay/replay_server' - require 'cosmos/gui/choosers/string_chooser' -end +require 'cosmos/tools/cmd_tlm_server/cmd_tlm_server_gui' module Cosmos - - class Replay < QtTool - # The number of bytes to print when an UNKNOWN packet is received - UNKNOWN_BYTES_TO_PRINT = 36 - - def initialize(options) - # MUST BE FIRST - All code before super is executed twice in RubyQt Based classes - super(options) - Cosmos.load_cosmos_icon("replay.png") - - initialize_actions() - initialize_menus() - initialize_central_widget() - complete_initialize() - - @ready = false - Splash.execute(self) do |splash| - ConfigParser.splash = splash - splash.message = "Initializing Replay Server" - - # Start the thread that will process server messages and add them to the output text - process_server_messages(options) - - ReplayServer.new(options.config_file, false, false, false) - @ready = true - - ConfigParser.splash = nil - end - - # Initialize variables - @packet_log_reader = System.default_packet_log_reader.new(*System.default_packet_log_reader_params) - @log_directory = System.paths['LOGS'] - @log_directory << '/' unless @log_directory[-1..-1] == '\\' or @log_directory[-1..-1] == '/' - @log_filename = nil - @playing = false - @playback_thread = nil - @playback_index = 0 - @packet_offsets = [] - end - - def initialize_menus - # File Menu - @file_menu = menuBar.addMenu(tr('&File')) - @file_menu.addAction(@exit_action) - - # Help Menu - @about_string = "Telemetry Viewer provides a view of every telemetry packet in the system." - @about_string << " Packets can be viewed in numerous represenations ranging from the raw data to formatted with units." - - 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 - - @log_widget = Qt::Widget.new - @log_widget.setSizePolicy(Qt::SizePolicy::MinimumExpanding, Qt::SizePolicy::MinimumExpanding) - @log_layout = Qt::VBoxLayout.new() - - # This widget goes inside the top layout so we want 0 contents margins - @log_layout.setContentsMargins(0,0,0,0) - @log_widget.setLayout(@log_layout) - - # Create the log file GUI - @log_file_selection = Qt::GroupBox.new("Log File Selection") - @log_select = Qt::HBoxLayout.new(@log_file_selection) - @log_name = Qt::LineEdit.new - @log_name.setReadOnly(true) - @log_select.addWidget(@log_name) - @log_open = Qt::PushButton.new("Browse...") - @log_select.addWidget(@log_open) - @log_layout.addWidget(@log_file_selection) - - @log_open.connect(SIGNAL('clicked()')) { select_log_file() } - - # Create the operation buttons GUI - @op = Qt::GroupBox.new(tr("Playback Control")) - @op_layout = Qt::VBoxLayout.new(@op) - @op_button_layout = Qt::HBoxLayout.new - @move_start = Qt::PushButton.new(Cosmos.get_icon('skip_to_start-26.png'), '') - @move_start.connect(SIGNAL('clicked()')) { move_start() } - @op_button_layout.addWidget(@move_start) - @step_back = Qt::PushButton.new(Cosmos.get_icon('rewind-26.png'), '') - @step_back_timer = Qt::Timer.new - @step_back_timeout = 100 - @step_back_timer.connect(SIGNAL('timeout()')) { step_back(); @step_back_timeout = (@step_back_timeout / 2).to_i; @step_back_timer.start(@step_back_timeout) } - @step_back.connect(SIGNAL('pressed()')) { step_back(); @step_back_timeout = 300; @step_back_timer.start(@step_back_timeout) } - @step_back.connect(SIGNAL('released()')) { @step_back_timer.stop } - @op_button_layout.addWidget(@step_back) - @reverse_play = Qt::PushButton.new(Cosmos.get_icon('reverse-play-26.png'), '') - @reverse_play.connect(SIGNAL('clicked()')) { reverse_play() } - @op_button_layout.addWidget(@reverse_play) - @stop = Qt::PushButton.new(Cosmos.get_icon('stop-26.png'), '') - @stop.connect(SIGNAL('clicked()')) { stop() } - @op_button_layout.addWidget(@stop) - @play = Qt::PushButton.new(Cosmos.get_icon('play-26.png'), '') - @play.connect(SIGNAL('clicked()')) { play() } - @op_button_layout.addWidget(@play) - @step_forward = Qt::PushButton.new(Cosmos.get_icon('fast_forward-26.png'), '') - @step_forward_timer = Qt::Timer.new - @step_forward_timeout = 100 - @step_forward_timer.connect(SIGNAL('timeout()')) { step_forward(); @step_forward_timeout = (@step_forward_timeout / 2).to_i; @step_forward_timer.start(@step_forward_timeout) } - @step_forward.connect(SIGNAL('pressed()')) { step_forward(); @step_forward_timeout = 300; @step_forward_timer.start(@step_forward_timeout) } - @step_forward.connect(SIGNAL('released()')) { @step_forward_timer.stop } - @op_button_layout.addWidget(@step_forward) - @move_end = Qt::PushButton.new(Cosmos.get_icon('end-26.png'), '') - @move_end.connect(SIGNAL('clicked()')) { move_end() } - @op_button_layout.addWidget(@move_end) - @op_layout.addLayout(@op_button_layout) - - # Speed Selection - @playback_delay = nil - @speed_select = Qt::ComboBox.new - @variants = [] - @variants << Qt::Variant.new(nil) - @speed_select.addItem("No Delay", @variants[-1]) - @variants << Qt::Variant.new(0.001) - @speed_select.addItem("1ms Delay", @variants[-1]) - @variants << Qt::Variant.new(0.002) - @speed_select.addItem("2ms Delay", @variants[-1]) - @variants << Qt::Variant.new(0.005) - @speed_select.addItem("5ms Delay", @variants[-1]) - @variants << Qt::Variant.new(0.01) - @speed_select.addItem("10ms Delay", @variants[-1]) - @variants << Qt::Variant.new(0.05) - @speed_select.addItem("50ms Delay", @variants[-1]) - @variants << Qt::Variant.new(0.125) - @speed_select.addItem("125ms Delay", @variants[-1]) - @variants << Qt::Variant.new(0.25) - @speed_select.addItem("250ms Delay", @variants[-1]) - @variants << Qt::Variant.new(0.5) - @speed_select.addItem("500ms Delay", @variants[-1]) - @variants << Qt::Variant.new(1.0) - @speed_select.addItem("1s Delay", @variants[-1]) - @variants << Qt::Variant.new(-1.0) - @speed_select.addItem("Realtime", @variants[-1]) - @speed_select.setMaxVisibleItems(11) - @speed_select.connect(SIGNAL('currentIndexChanged(int)')) do - @playback_delay = @speed_select.itemData(@speed_select.currentIndex).value - end - @speed_layout = Qt::FormLayout.new() - @speed_layout.addRow("&Delay:", @speed_select) - @status = Qt::LineEdit.new - @status.setReadOnly(true) - @status.setText('Stopped') - @speed_layout.addRow("&Status:", @status) - @op_layout.addLayout(@speed_layout) - @log_layout.addWidget(@op) - - @file_pos = Qt::GroupBox.new(tr("File Position")) - @file_pos_layout = Qt::VBoxLayout.new(@file_pos) - @slider = Qt::Slider.new(Qt::Horizontal) - @slider.setRange(0, 10000) - @slider.setTickInterval(1000) - @slider.setTickPosition(Qt::Slider::TicksBothSides) - @slider.setTracking(false) - @slider.connect(SIGNAL('sliderReleased()')) { slider_released() } - @time_layout = Qt::HBoxLayout.new() - @start_time = StringChooser.new(self, 'Start:', '', 200, true, true, Qt::AlignCenter | Qt::AlignVCenter) - @end_time = StringChooser.new(self, 'End:', '', 200, true, true, Qt::AlignCenter | Qt::AlignVCenter) - @current_time = StringChooser.new(self, 'Current:', '', 200, true, true, Qt::AlignCenter | Qt::AlignVCenter) - @time_layout.addWidget(@start_time) - @time_layout.addWidget(@current_time) - @time_layout.addWidget(@end_time) - @file_pos_layout.addLayout(@time_layout) - @file_pos_layout.addWidget(@slider) - @log_layout.addWidget(@file_pos) - @top_layout.addWidget(@log_widget) - - # Add the message output - @output = Qt::PlainTextEdit.new - @output.setReadOnly(true) - @output.setMaximumBlockCount(100) - - @top_layout.addWidget(@output, 500) - - # Override stdout to the message window - # All code attempting to print into the GUI must use $stdout rather than STDOUT - @string_output = StringIO.new("", "r+") - $stdout = @string_output - Logger.level = Logger::INFO - - @central_widget.setLayout(@top_layout) - end - - def select_log_file - unless @playback_thread - packet_log_dialog = PacketLogDialog.new( - self, 'Select Log File', @log_directory, @packet_log_reader, - [], nil, false, false, true, Cosmos::TLM_FILE_PATTERN, - Cosmos::BIN_FILE_PATTERN, false - ) - case packet_log_dialog.exec - when Qt::Dialog::Accepted - stop() - @packet_log_reader = packet_log_dialog.packet_log_reader - @log_filename = packet_log_dialog.filenames[0] - @log_directory = File.dirname(@log_filename) - @log_directory << '/' unless @log_directory[-1..-1] == '\\' - @log_name.text = @log_filename - - System.telemetry.reset - @cancel = false - ProgressDialog.execute(self, 'Analyzing Log File', 500, 10, true, false, true, false, true) do |progress_dialog| - progress_dialog.append_text("Processing File: #{@log_filename}\n") - progress_dialog.set_overall_progress(0.0) - progress_dialog.cancel_callback = method(:cancel_callback) - progress_dialog.enable_cancel_button - Cosmos.check_log_configuration(@packet_log_reader, @log_filename) - @packet_offsets = @packet_log_reader.packet_offsets(@log_filename, lambda {|percentage| progress_dialog.set_overall_progress(percentage); @cancel}) - @playback_index = 0 - update_slider_and_current_time(nil) - @packet_log_reader.open(@log_filename) - progress_dialog.close_done - end - - if ProgressDialog.canceled? - @packet_log_reader.close - @log_name.text = '' - @log_filename = nil - @packet_offsets = [] - @playback_index = 0 - @start_time.value = '' - @current_time.value = '' - @end_time.value = '' - else - move_end() - move_start() - end - end - end - end - - def cancel_callback(progress_dialog = nil) - @cancel = true - return true, false - end - - def move_start - if @log_filename and !@playback_thread - packet = read_at_index(0, :FORWARD) - @start_time.value = packet.received_time.formatted(true, 3, true) if packet and packet.received_time - else - stop() - end - end - - def step_back - if @log_filename and !@playback_thread - @playback_index = @packet_offsets.length - 2 if @playback_index >= @packet_offsets.length - read_at_index(@playback_index, :BACKWARD) - else - stop() - end - end - - def reverse_play - if @log_filename and !@playback_thread - @playback_index = @packet_offsets.length - 2 if @playback_index >= @packet_offsets.length - start_playback(:BACKWARD) - else - stop() - end - end - - def stop - @playing = false - end - - def play - if @log_filename and !@playback_thread - @playback_index = 1 if @playback_index < 0 - start_playback(:FORWARD) - else - stop() - end - end - - def step_forward - if @log_filename and !@playback_thread - @playback_index = 1 if @playback_index < 0 - read_at_index(@playback_index, :FORWARD) - else - stop() - end - end - - def move_end - if @log_filename and !@playback_thread - packet = read_at_index(@packet_offsets.length - 1, :FORWARD) - @end_time.value = packet.received_time.formatted(true, 3, true) if packet and packet.received_time - else - stop() - end - end - - def slider_released - if @log_filename and !@playback_thread - read_at_index(((@slider.sliderPosition / 10000.0) * (@packet_offsets.length - 1)).to_i, :FORWARD) - end - end - - def start_playback(direction) - @playback_thread = Thread.new do - error = nil - begin - @playing = true - Qt.execute_in_main_thread(true) do - @status.setText('Playing') - end - previous_packet = nil - while (@playing) - if @playback_delay - packet_start = Time.now.sys - packet = read_at_index(@playback_index, direction) - break unless packet - delay_time = 0.0 - if @playback_delay > 0.0 - delay_time = @playback_delay - (Time.now.sys - packet_start) - elsif previous_packet and packet.received_time and previous_packet.received_time - if direction == :FORWARD - delay_time = packet.received_time - previous_packet.received_time - (Time.now.sys - packet_start) - else - delay_time = previous_packet.received_time - packet.received_time - (Time.now.sys - packet_start) - end - end - sleep(delay_time) if delay_time > 0.0 - previous_packet = packet - else - packet = read_at_index(@playback_index, direction) - break unless packet - end - end - rescue Exception => error - Qt.execute_in_main_thread(true) {|| ExceptionDialog.new(self, error, "Playback Thread")} - ensure - Qt.execute_in_main_thread(true) do - @status.setText('Stopped') - end - @playing = false - @playback_thread = nil - end - end - end - - def read_at_index(index, direction) - packet_offset = nil - packet_offset = @packet_offsets[index] if index >= 0 - if packet_offset - # Read the packet - packet = @packet_log_reader.read_at_offset(packet_offset, false) - handle_packet(packet) - - # Adjust index for next read - if direction == :FORWARD - @playback_index = index + 1 - else - @playback_index = index - 1 - end - update_slider_and_current_time(packet) - - return packet - else - return nil - end - end - - def update_slider_and_current_time(packet) - Qt.execute_in_main_thread(false) do - value = (((@playback_index - 1) / @packet_offsets.length.to_f) * 10000).to_i - @slider.setSliderPosition(value) - @slider.setValue(value) - @current_time.value = packet.received_time.formatted(true, 3, true) if packet and packet.received_time - end - end - - def handle_packet(packet) - # For replay we will try our best here but not crash on errors - begin - interface = nil - - # Identify and update packet - if packet.identified? - # Preidentifed packet - place it into the current value table - identified_packet = System.telemetry.update!(packet.target_name, - packet.packet_name, - packet.buffer) - else - # Packet needs to be identified - identified_packet = System.telemetry.identify!(packet.buffer) - end - - if identified_packet and packet.target_name != 'UNKNOWN' - identified_packet.received_time = packet.received_time - packet = identified_packet - target = System.targets[packet.target_name.upcase] - interface = target.interface if target - else - unknown_packet = System.telemetry.update!('UNKNOWN', 'UNKNOWN', packet.buffer) - unknown_packet.received_time = packet.received_time - packet = unknown_packet - data_length = packet.length - string = "Unknown #{data_length} byte packet starting: " - num_bytes_to_print = [UNKNOWN_BYTES_TO_PRINT, data_length].min - data_to_print = packet.buffer(false)[0..(num_bytes_to_print - 1)] - data_to_print.each_byte do |byte| - string << sprintf("%02X", byte) - end - time_string = '' - time_string = packet.received_time.formatted << ' ' if packet.received_time - puts "#{time_string}ERROR: #{string}" - end - - target = System.targets[packet.target_name] - target.tlm_cnt += 1 if target - packet.received_count += 1 - packet.check_limits(System.limits_set) - ReplayServer.instance.post_packet(packet) - - # Write to routers - if interface - interface.routers.each do |router| - begin - router.write(packet) if router.write_allowed? and router.connected? - rescue => err - Logger.error "Problem writing to router #{router.name} - #{err.class}:#{err.message}" - end - end - end - rescue Exception => err - Logger.error "Problem handling packet #{packet.target_name} #{packet.packet_name} - #{err.class}:#{err.message}" - end - end - - def process_server_messages(options) - # Start thread to read server messages - @output_thread = Thread.new do - begin - while !@ready - sleep(1) - end - while true - if @string_output.string[-1..-1] == "\n" - Qt.execute_in_main_thread(true) do - string = @string_output.string.clone - @string_output.string = @string_output.string[string.length..-1] - string.each_line {|out_line| @output.add_formatted_text(out_line) } - @output.flush - end - end - sleep(1) - end - rescue Exception => error - Qt.execute_in_main_thread(true) do - ExceptionDialog.new(self, error, "#{options.title}: Messages Thread") - end - end - end - end - - def closeEvent(event) - Cosmos.kill_thread(self, @playback_thread) - super(event) - end - - # Gracefully kill threads - def graceful_kill - stop() - end - - 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.title = 'Replay' - options.width = 800 - options.height = 500 - options.auto_size = false - options.config_file = CmdTlmServer::DEFAULT_CONFIG_FILE - option_parser.separator "Replay Specific Options:" - option_parser.on("-c", "--config FILE", "Use the specified configuration file") do |arg| - options.config_file = arg - end - end - - super(option_parser, options) - end - end - - end # class Replay - -end # module Cosmos + # Implements the GUI functions of the Command and Telemetry Server. All the + # QT calls are implemented here. The non-GUI functionality is contained in + # the CmdTlmServer class. + class Replay < CmdTlmServerGui + end +end diff --git a/lib/cosmos/tools/replay/replay_server.rb b/lib/cosmos/tools/replay/replay_server.rb deleted file mode 100644 index 9026aebfa..000000000 --- a/lib/cosmos/tools/replay/replay_server.rb +++ /dev/null @@ -1,91 +0,0 @@ -# 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/tools/cmd_tlm_server/cmd_tlm_server' - -module Cosmos - - class ReplayServer < CmdTlmServer - - # Start up the system by starting the JSON-RPC server, interfaces, routers, - # and background tasks. Starts a thread to monitor all packets for - # staleness so other tools (such as Packet Viewer or Telemetry Viewer) can - # react accordingly. - # - # @param production (see #initialize) - def start(production = false) - # Prevent access to interfaces or packet_logging - @interfaces = nil - @packet_logging = nil - - System.telemetry # Make sure definitions are loaded by starting anything - return unless @json_drb.nil? - - # Start DRb with access control - @json_drb = JsonDRb.new - @json_drb.acl = System.acl if System.acl - - @json_drb.method_whitelist = @api_whitelist - begin - @json_drb.start_service(System.listen_hosts['CTS_API'], System.ports['CTS_API'], self) - rescue Exception - raise FatalError.new("Error starting JsonDRb on port #{System.ports['CTS_API']}.\nPerhaps a Command and Telemetry Server is already running?") - end - - @routers.add_preidentified('PREIDENTIFIED_ROUTER', System.instance.ports['CTS_PREIDENTIFIED']) - System.telemetry.limits_change_callback = method(:limits_change_callback) - @routers.start - end - - # Properly shuts down the command and telemetry server by stoping the - # JSON-RPC server, background tasks, routers, and interfaces. Also kills - # the packet staleness monitor thread. - def stop - # Shutdown DRb - @json_drb.stop_service - @routers.stop - @json_drb = nil - end - - # Called when an item in any packet changes limits states. - # - # @param packet [Packet] Packet which has had an item change limits state - # @param item [PacketItem] The item which has changed limits state - # @param old_limits_state [Symbol] The previous state of the item. See - # {PacketItemLimits#state} - # @param value [Object] The current value of the item - # @param log_change [Boolean] Whether to log this limits change event - def limits_change_callback(packet, item, old_limits_state, value, log_change) - if log_change - # Write to Server Messages that limits state has changed - tgt_pkt_item_str = "#{packet.target_name} #{packet.packet_name} #{item.name} = #{value} is" - time_string = '' - time_string = packet.received_time.formatted << ' ' if packet.received_time - - case item.limits.state - when :BLUE - puts "#{time_string}INFO: #{tgt_pkt_item_str} #{item.limits.state}" - when :GREEN, :GREEN_LOW, :GREEN_HIGH - puts "#{time_string}INFO: #{tgt_pkt_item_str} #{item.limits.state}" - when :YELLOW, :YELLOW_LOW, :YELLOW_HIGH - puts "#{time_string}WARN: #{tgt_pkt_item_str} #{item.limits.state}" - when :RED, :RED_LOW, :RED_HIGH - puts "#{time_string}ERROR: #{tgt_pkt_item_str} #{item.limits.state}" - else - puts "ERROR: #{tgt_pkt_item_str} UNKNOWN" - end - end - - post_limits_event(:LIMITS_CHANGE, [packet.target_name, packet.packet_name, item.name, old_limits_state, item.limits.state]) - end - - end # class ReplayServer - -end # module Cosmos diff --git a/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_realtime_thread.rb b/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_realtime_thread.rb index 35428eb11..b1fca7477 100644 --- a/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_realtime_thread.rb +++ b/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_realtime_thread.rb @@ -18,8 +18,12 @@ module Cosmos class TabbedPlotsRealtimeThread < InterfaceThread # Create a new TabbedPlotsRealtimeThread - def initialize(tabbed_plots_config, connection_success_callback = nil, connection_failed_callback = nil, connection_lost_callback = nil, fatal_exception_callback = nil) - interface = TcpipClientInterface.new(System.connect_hosts['CTS_PREIDENTIFIED'], nil, System.ports['CTS_PREIDENTIFIED'], nil, tabbed_plots_config.cts_timeout, 'PREIDENTIFIED') + def initialize(tabbed_plots_config, connection_success_callback = nil, connection_failed_callback = nil, connection_lost_callback = nil, fatal_exception_callback = nil, replay_mode = false) + if replay_mode + interface = TcpipClientInterface.new(System.connect_hosts['REPLAY_PREIDENTIFIED'], nil, System.ports['REPLAY_PREIDENTIFIED'], nil, tabbed_plots_config.cts_timeout, 'PREIDENTIFIED') + else + interface = TcpipClientInterface.new(System.connect_hosts['CTS_PREIDENTIFIED'], nil, System.ports['CTS_PREIDENTIFIED'], nil, tabbed_plots_config.cts_timeout, 'PREIDENTIFIED') + end super(interface) @queue = Queue.new diff --git a/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_tool.rb b/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_tool.rb index ef3943cb9..11aed0381 100644 --- a/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_tool.rb +++ b/lib/cosmos/tools/tlm_grapher/tabbed_plots_tool/tabbed_plots_tool.rb @@ -63,6 +63,7 @@ def initialize(options) @tabbed_plots = nil @realtime_thread = nil @config_modified = false + @replay_mode = false # Bring up slash screen for long duration tasks after creation Splash.execute(self) do |splash| @@ -107,6 +108,10 @@ def initialize_actions @file_screenshot.statusTip = tr('Screenshot of Application') @file_screenshot.connect(SIGNAL('triggered()')) { on_file_screenshot() } + @replay_action = Qt::Action.new(tr('Toggle Replay Mode'), self) + @replay_action.statusTip = tr('Toggle Replay Mode') + @replay_action.connect(SIGNAL('triggered()')) { toggle_replay_mode() } + # Tab Menu Actions @tab_add = Qt::Action.new(Cosmos.get_icon('add_tab.png'), tr('&Add Tab'), self) @tab_add.statusTip = tr('Add New Tab') @@ -209,6 +214,8 @@ def initialize_menus(options) @file_menu.addSeparator() @file_menu.addAction(@file_screenshot) @file_menu.addSeparator() + @file_menu.addAction(@replay_action) + @file_menu.addSeparator() @file_menu.addAction(@exit_action) @tab_menu = menuBar.addMenu(tr('&Tab')) @@ -265,6 +272,10 @@ def initialize_central_widget # Create a Vertical Frame for the right contents @right_widget = Qt::Widget.new(self) @right_frame = Qt::VBoxLayout.new + @replay_flag = Qt::Label.new("Replay Mode") + @replay_flag.setStyleSheet("background:green;color:white;padding:5px;font-weight:bold;height:30px;") + @right_frame.addWidget(@replay_flag) + @replay_flag.hide @right_widget.setLayout(@right_frame) @splitter.addWidget(@right_widget) @splitter.setStretchFactor(0,0) # Set the left side stretch factor to 0 @@ -581,6 +592,20 @@ def on_file_screenshot @tabbed_plots.resume unless paused end # def on_file_screenshot + def toggle_replay_mode + running = @realtime_thread ? true : false + handle_stop() + System.telemetry.reset + @tabbed_plots.reset_all_data_objects + @replay_mode = !@replay_mode + if @replay_mode + @replay_flag.show + else + @replay_flag.hide + end + handle_start() if running + end + ############################################################################### # Tab Menu Handlers ############################################################################### @@ -921,7 +946,7 @@ def handle_start # Startup realtime thread @realtime_button_bar.state = 'Connecting' statusBar.showMessage(tr("Connecting to COSMOS Server")) - @realtime_thread = TabbedPlotsRealtimeThread.new(@tabbed_plots_config, method(:realtime_thread_connection_success_callback), method(:realtime_thread_connection_failed_callback), method(:realtime_thread_connection_lost_callback), method(:realtime_thread_fatal_exception_callback)) + @realtime_thread = TabbedPlotsRealtimeThread.new(@tabbed_plots_config, method(:realtime_thread_connection_success_callback), method(:realtime_thread_connection_failed_callback), method(:realtime_thread_connection_lost_callback), method(:realtime_thread_fatal_exception_callback), @replay_mode) end end # def handle_start diff --git a/lib/cosmos/tools/tlm_viewer/screen.rb b/lib/cosmos/tools/tlm_viewer/screen.rb index da0fb3112..9b98f4cdb 100644 --- a/lib/cosmos/tools/tlm_viewer/screen.rb +++ b/lib/cosmos/tools/tlm_viewer/screen.rb @@ -21,7 +21,7 @@ class Screen < Qt::MainWindow # close_all_screens is called @@open_screens = [] - attr_accessor :full_name, :width, :height, :window + attr_accessor :full_name, :width, :height, :window, :replay_flag class Widgets # Flag to indicate all screens should close @@ -229,6 +229,7 @@ def initialize(full_name, filename, notify_on_close = nil, mode = :REALTIME, x_p app_style = File.join(Cosmos::USERPATH, 'config', 'tools', 'application.css') setStyleSheet(File.read(app_style)) if File.exist? app_style + @replay_flag = nil @widgets = Widgets.new(self, mode) @window = process(filename) @@open_screens << self if @window @@ -277,6 +278,12 @@ def process(filename) setCentralWidget(top_widget) frame = Qt::VBoxLayout.new() top_widget.setLayout(frame) + + @replay_flag = Qt::Label.new("Replay Mode") + @replay_flag.setStyleSheet("background:green;color:white;padding:5px;font-weight:bold;") + frame.addWidget(@replay_flag) + @replay_flag.hide unless get_replay_mode() + layout_stack[0] = frame Cosmos.load_cosmos_icon if @single_screen when 'END' @@ -498,5 +505,21 @@ def self.close_all_screens(closer) Widgets.closing_all = false end + def self.update_replay_mode + screens = @@open_screens.clone + replay_mode = get_replay_mode() + screens.each do |screen| + begin + if replay_mode + screen.replay_flag.show if screen.replay_flag + else + screen.replay_flag.hide if screen.replay_flag + end + rescue + # Oh well + end + end + end + end end diff --git a/lib/cosmos/tools/tlm_viewer/tlm_viewer.rb b/lib/cosmos/tools/tlm_viewer/tlm_viewer.rb index 3616d774e..440697acd 100644 --- a/lib/cosmos/tools/tlm_viewer/tlm_viewer.rb +++ b/lib/cosmos/tools/tlm_viewer/tlm_viewer.rb @@ -179,6 +179,10 @@ def initialize_actions @file_audit.shortcut = @file_audit_keyseq @file_audit.statusTip = tr('Create a report listing which telemetry points are not on screens') @file_audit.connect(SIGNAL('triggered()')) { file_audit() } + + @replay_action = Qt::Action.new(tr('Toggle Replay Mode'), self) + @replay_action.statusTip = tr('Toggle Replay Mode') + @replay_action.connect(SIGNAL('triggered()')) { toggle_replay_mode() } end def initialize_menus(options) @@ -187,6 +191,7 @@ def initialize_menus(options) @file_menu.addAction(@file_save) @file_menu.addAction(@file_generate) @file_menu.addAction(@file_audit) + @file_menu.addAction(@replay_action) @file_menu.addSeparator() @file_menu.addAction(@exit_action) @@ -202,6 +207,11 @@ def initialize_central_widget(options) setCentralWidget(central_widget) top_layout = Qt::VBoxLayout.new + @replay_flag = Qt::Label.new("Replay Mode") + @replay_flag.setStyleSheet("background:green;color:white;padding:5px;font-weight:bold;") + top_layout.addWidget(@replay_flag) + @replay_flag.hide + @search_box = FullTextSearchLineEdit.new(self) top_layout.addWidget(@search_box) @@ -406,6 +416,16 @@ def file_audit Cosmos.open_in_text_editor(output_filename) if output_filename end + def toggle_replay_mode + set_replay_mode(!get_replay_mode()) + if get_replay_mode() + @replay_flag.show + else + @replay_flag.hide + end + Screen.update_replay_mode + end + # Method called by screens to notify that they have been closed def notify(closed_screen) screen_full_name = closed_screen.full_name diff --git a/lib/cosmos/tools/tlm_viewer/tlm_viewer_config.rb b/lib/cosmos/tools/tlm_viewer/tlm_viewer_config.rb index ea8bc5df6..a83f29919 100644 --- a/lib/cosmos/tools/tlm_viewer/tlm_viewer_config.rb +++ b/lib/cosmos/tools/tlm_viewer/tlm_viewer_config.rb @@ -46,6 +46,21 @@ def initialize(group, target_name, name, filename, x_pos, y_pos) @invalid_items = [] end + def as_json(options = nil) #:nodoc: + {group: @group, + target_name: @target_name, + original_target_name: + @original_target_name, + name: @name, + filename: @filename, + x_pos: @x_pos, + y_pos: @y_pos, + substitute: @substitute, + force_substitute: @force_substitute, + show_on_startup: @show_on_startup + } + end + def full_name @group ? @name : "#{@target_name} #{@name}" end @@ -96,7 +111,7 @@ def read_items attr_accessor :completion_list attr_accessor :tlm_to_screen_mapping - def initialize(filename = nil) + def initialize(filename = nil, skip_read_items = false) # Handle nil filename filename = File.join(Cosmos::USERPATH, 'config', 'tools', 'tlm_viewer', 'tlm_viewer.txt') unless filename @filename = filename @@ -129,7 +144,7 @@ def initialize(filename = nil) screen_dir = File.join(target.dir, 'screens') if File.exist?(screen_dir) and num_screens(screen_dir) > 0 start_target(target.name, parser) - auto_screens() + auto_screens(skip_read_items) end end @@ -141,7 +156,7 @@ def initialize(filename = nil) screen_dir = File.join(target.dir, 'screens') if File.exist?(screen_dir) and num_screens(screen_dir) > 0 start_target(target.name, parser) - auto_screens() + auto_screens(skip_read_items) end when 'TARGET' @@ -153,7 +168,7 @@ def initialize(filename = nil) raise parser.error("No target defined. SCREEN must follow TARGET.") unless @current_target parser.verify_num_parameters(1, 3, 'SCREEN ') screen_filename = File.join(@current_target.dir, 'screens', parameters[0]) - start_screen(screen_filename, parameters[1], parameters[2]) + start_screen(screen_filename, parameters[1], parameters[2], skip_read_items) when 'SHOW_ON_STARTUP' raise parser.error("No screen defined. SHOW_ON_STARTUP must follow SCREEN or GROUP_SCREEN.") unless @current_screen_info @@ -179,7 +194,7 @@ def initialize(filename = nil) parser.verify_num_parameters(2, 4, 'GROUP_SCREEN ') start_target(parameters[0].upcase, parser, @current_group) screen_filename = File.join(@current_target.dir, 'screens', parameters[1]) - start_screen(screen_filename, parameters[2], parameters[3]) + start_screen(screen_filename, parameters[2], parameters[3], skip_read_items) else # blank config.lines will have a nil keyword and should not raise an exception @@ -233,7 +248,7 @@ def start_target(target_name, parser, group = nil) end end - def start_screen(screen_filename, x_pos = nil, y_pos = nil) + def start_screen(screen_filename, x_pos = nil, y_pos = nil, skip_read_items = false) screen_name = File.basename(screen_filename, '.txt').upcase x_pos = x_pos.to_i if x_pos y_pos = y_pos.to_i if y_pos @@ -244,15 +259,15 @@ def start_screen(screen_filename, x_pos = nil, y_pos = nil) @current_screen_info.force_substitute = true if @current_target.auto_screen_substitute @current_screen_info.original_target_name = @current_target.original_name @current_screen_info.substitute = @current_target.name if @current_target.substitute or @current_target.auto_screen_substitute - @current_screen_info.read_items + @current_screen_info.read_items unless skip_read_items end - def auto_screens + def auto_screens(skip_read_items = false) @current_group = nil screen_dir = File.join(@current_target.dir, 'screens') if File.exist?(screen_dir) Dir.new(screen_dir).each do |filename| - start_screen(File.join(screen_dir, filename)) if valid_screen_name(filename) + start_screen(File.join(screen_dir, filename), nil, nil, skip_read_items) if valid_screen_name(filename) end end end diff --git a/spec/interfaces/cmd_tlm_server_interface_spec.rb b/spec/interfaces/cmd_tlm_server_interface_spec.rb index 2ad1e5280..86a7b190e 100644 --- a/spec/interfaces/cmd_tlm_server_interface_spec.rb +++ b/spec/interfaces/cmd_tlm_server_interface_spec.rb @@ -32,8 +32,7 @@ module Cosmos allow_any_instance_of(Interface).to receive(:connected?).and_return(true) allow_any_instance_of(Interface).to receive(:disconnect) @cts = CmdTlmServer.new - @cts.start - sleep 0.1 + sleep 1 @ctsi = CmdTlmServerInterface.new end diff --git a/spec/packets/packet_item_spec.rb b/spec/packets/packet_item_spec.rb index d3ea06f88..adfa88608 100644 --- a/spec/packets/packet_item_spec.rb +++ b/spec/packets/packet_item_spec.rb @@ -324,7 +324,7 @@ module Cosmos describe "clone" do it "duplicates the entire PacketItem" do pi2 = @pi.clone - expect(@pi == pi2).to be true + expect(@pi < pi2).to be true end end diff --git a/spec/packets/structure_item_spec.rb b/spec/packets/structure_item_spec.rb index 0cc0cb1cf..7fbbbb6c2 100644 --- a/spec/packets/structure_item_spec.rb +++ b/spec/packets/structure_item_spec.rb @@ -149,8 +149,8 @@ module Cosmos expect(si1 > si2).to be false si2 = StructureItem.new("si2", 0, 8, :UINT, :BIG_ENDIAN, nil) - expect(si1 < si2).to be false - expect(si1 == si2).to be true + expect(si1 < si2).to be true + expect(si1 == si2).to be false expect(si1 > si2).to be false end @@ -170,9 +170,8 @@ module Cosmos expect(si1 > si2).to be true si2 = StructureItem.new("si2", -8, 8, :UINT, :BIG_ENDIAN, nil) - expect(si1 < si2).to be false - # si1 == si2 even though they have different names and sizes - expect(si1 == si2).to be true + expect(si1 < si2).to be true + expect(si1 == si2).to be false expect(si1 > si2).to be false end @@ -189,7 +188,7 @@ module Cosmos it "duplicates the entire structure item " do si1 = StructureItem.new("si1", -8, 1, :UINT, :LITTLE_ENDIAN, nil) si2 = si1.clone - expect(si1 == si2).to be true + expect(si1 < si2).to be true end end diff --git a/spec/script/telemetry_spec.rb b/spec/script/telemetry_spec.rb index c904e3d1e..f0cf85c20 100644 --- a/spec/script/telemetry_spec.rb +++ b/spec/script/telemetry_spec.rb @@ -168,6 +168,11 @@ module Cosmos expect(packet.target_name).to eql "SYSTEM" expect(packet.packet_name).to eql "META" expect(packet.received_time).to be_within(1).of Time.now + expect(packet.received_count).to eql 0 + packet = get_packet(id) + expect(packet.target_name).to eql "SYSTEM" + expect(packet.packet_name).to eql "META" + expect(packet.received_time).to be_within(1).of Time.now expect(packet.received_count).to eql 1 unsubscribe_packet_data(id) end diff --git a/spec/system/system_spec.rb b/spec/system/system_spec.rb index 4cefadc60..772868f9e 100644 --- a/spec/system/system_spec.rb +++ b/spec/system/system_spec.rb @@ -44,7 +44,7 @@ module Cosmos describe "instance" do it "creates default ports" do # Don't check the actual port numbers but just that they exist - expect(System.ports.keys).to eql %w(CTS_API TLMVIEWER_API CTS_PREIDENTIFIED CTS_CMD_ROUTER) + expect(System.ports.keys).to eql %w(CTS_API TLMVIEWER_API CTS_PREIDENTIFIED CTS_CMD_ROUTER REPLAY_API REPLAY_PREIDENTIFIED REPLAY_CMD_ROUTER) end it "creates default paths" do diff --git a/spec/tools/cmd_tlm_server/cmd_tlm_server_spec.rb b/spec/tools/cmd_tlm_server/cmd_tlm_server_spec.rb index aadd42ff0..5ebcc970a 100644 --- a/spec/tools/cmd_tlm_server/cmd_tlm_server_spec.rb +++ b/spec/tools/cmd_tlm_server/cmd_tlm_server_spec.rb @@ -83,19 +83,12 @@ module Cosmos expect(CmdTlmServer.json_drb.method_whitelist).to include('stop_logging') expect(CmdTlmServer.json_drb.method_whitelist).to include('stop_cmd_log') expect(CmdTlmServer.json_drb.method_whitelist).to include('stop_tlm_log') - cts.start # Call start again ... it should do nothing - expect(CmdTlmServer.json_drb.method_whitelist).to include('start_logging') - expect(CmdTlmServer.json_drb.method_whitelist).to include('stop_logging') - expect(CmdTlmServer.json_drb.method_whitelist).to include('stop_cmd_log') - expect(CmdTlmServer.json_drb.method_whitelist).to include('stop_tlm_log') ensure cts.stop sleep 0.2 end - expect_any_instance_of(PacketLogging).to receive(:start) - # Now start the server in production mode - cts.start(true) + cts = CmdTlmServer.new(CmdTlmServer::DEFAULT_CONFIG_FILE, true) begin # Verify we disabled the ability to stop logging expect(CmdTlmServer.json_drb.method_whitelist).to include('start_logging') diff --git a/spec/tools/table_manager/table_item_spec.rb b/spec/tools/table_manager/table_item_spec.rb index 4da1b8554..6df34b18f 100644 --- a/spec/tools/table_manager/table_item_spec.rb +++ b/spec/tools/table_manager/table_item_spec.rb @@ -35,7 +35,7 @@ module Cosmos describe "clone" do it "duplicates the entire TableItem" do pi2 = @ti.clone - expect(@ti == pi2).to be true + expect(@ti < pi2).to be true end end diff --git a/spec/top_level/top_level_spec.rb b/spec/top_level/top_level_spec.rb index 109970439..0c447dd33 100644 --- a/spec/top_level/top_level_spec.rb +++ b/spec/top_level/top_level_spec.rb @@ -238,7 +238,13 @@ def self.cleanup_exceptions it "creates a log file in System LOGS" do filename1 = Cosmos.create_log_file('test') expect(File.exist?(filename1)).to be true - expect(File.dirname(filename1)).to eq System.paths['LOGS'] + if File.dirname(filename1)[-1] == '/' and System.paths['LOGS'][-1] != '/' + expect(File.dirname(filename1)).to eq (System.paths['LOGS'] + '/') + elsif File.dirname(filename1)[-1] != '/' and System.paths['LOGS'][-1] == '/' + expect(File.dirname(filename1) + '/').to eq System.paths['LOGS'] + else + expect(File.dirname(filename1)).to eq System.paths['LOGS'] + end File.delete(filename1) end