Class: Cosmos::CmdSequence

Inherits:
QtTool show all
Defined in:
lib/cosmos/tools/cmd_sequence/cmd_sequence.rb

Overview

Creates and executes command sequences. Commands are choosen through a GUI similar to CmdSender where the user selects a target, command, and then sets the command parameters in a table layout. Sequences can be saved and loaded and can have relative or absolute delays.

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from QtTool

#about, #complete_initialize, config_path, create_default_options, graceful_kill, #initialize_help_menu, normalize_config_options, post_options_parsed_hook, pre_window_new_hook, redirect_io, restore_io, #target_dirs_action

Constructor Details

#initialize(options) ⇒ CmdSequence

Creates the CmdSequence instance

Parameters:

  • options (OpenStruct)

    Application command line options



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 53

def initialize(options)
  # MUST BE FIRST - All code before super is executed twice in RubyQt Based classes
  super(options)
  Cosmos.load_cosmos_icon("cmd_sequence.png")
  if options.output_dir
    @sequence_dir = options.output_dir
  else
    @sequence_dir = System.paths['SEQUENCES']
  end
  @filename = "Untitled"
  @run_thread = nil
  @exporter = nil

  begin
    process_config(options.config_file) if options.config_file
  rescue => error
    ExceptionDialog.new(self, error, "Error parsing #{options.config_file}")
  end

  initialize_actions()
  initialize_menus()
  initialize_central_widget()
  complete_initialize() # defined in qt_tool
  update_title()

  # Bring up slash screen for long duration tasks after creation
  Splash.execute(self) do |splash|
    # Configure CosmosConfig to interact with splash screen
    ConfigParser.splash = splash

    System.commands
    Qt.execute_in_main_thread(true) do
      update_targets()
      @target_select.setCurrentText(options.packet[0]) if options.packet
      update_commands()
      @cmd_select.setCurrentText(options.packet[1]) if options.packet

      # Handle searching entries
      @search_box.completion_list = System.commands.all_packet_strings(true, splash)
      @search_box.callback = lambda do |cmd|
        split_cmd = cmd.split(" ")
        if split_cmd.length == 2
          target_name = split_cmd[0].upcase
          @target_select.setCurrentText(target_name)
          update_commands()
          command_name = split_cmd[1]
          @cmd_select.setCurrentText(command_name)
          add_command()
        end
      end
    end
    # Unconfigure CosmosConfig to interact with splash screen
    ConfigParser.splash = nil
  end
  run_sequence(options.run_sequence) if options.run_sequence
end

Class Method Details

.run(option_parser = nil, options = nil) ⇒ Object

Runs the CmdSequence application



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 32

def self.run(option_parser = nil, options = nil)
  Cosmos.catch_fatal_exception do
    unless option_parser && options
      option_parser, options = create_default_options()
      options.width = 600
      options.height = 425
      option_parser.separator "Command Sequence Specific Options:"
      option_parser.on("-o", "--output DIRECTORY", "Save files in the specified directory") do |arg|
        options.output_dir = File.expand_path(arg)
      end
      options.run_sequence = nil
      option_parser.on("-r", "--run FILE", "Open and run the specified sequence") do |arg|
        options.run_sequence = arg
      end
    end
    super(option_parser, options)
  end
end

Instance Method Details

#add_commandObject



282
283
284
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 282

def add_command
  item = @sequence_list.add(@target_select.text.strip, @cmd_select.text.strip)
end

#closeEvent(event) ⇒ Object

Handle the closeEvent to check if we’re running or a sequence needs to be saved before closing. Must be part of the public API.



467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 467

def closeEvent(event)
  if prompt_if_running_on_close()
    handle_stop()
    if prompt_for_save_if_needed()
      super(event)
    else
      event.ignore()
    end
  else
    event.ignore()
  end
end

#file_exportObject

Export the sequence list into a custom binary format



287
288
289
290
291
292
293
294
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 287

def file_export
  return if @sequence_list.empty? || @exporter.nil?
  begin
    @exporter.export(@filename, @sequence_dir, @sequence_list)
  rescue => error
    ExceptionDialog.new(self, error, 'Export Error', false)
  end
end

#file_newObject

Clears the sequence list



297
298
299
300
301
302
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 297

def file_new
  return unless prompt_for_save_if_needed()
  @sequence_list.clear
  @filename = "Untitled"
  update_title()
end

#file_open(filename = nil) ⇒ Object

Opens a sequence list file and populates the GUI

Parameters:

  • filename (String) (defaults to: nil)

    Name of the file to open



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 306

def file_open(filename = nil)
  return unless prompt_for_save_if_needed()
  if File.directory?(filename)
    filename = Qt::FileDialog.getOpenFileName(self, "Select Sequence", filename)
  else
    filename = Qt::FileDialog.getOpenFileName(self, "Select Sequence")
  end
  if !filename.nil? && File.exist?(filename) && !File.directory?(filename)
    # Try to open and load the file. Errors are handled here.
    @sequence_list.open(filename)
    @filename = filename
    @sequence_dir = File.dirname(filename)
    @sequence_dir << '/' if @sequence_dir[-1..-1] != '/' and @sequence_dir[-1..-1] != '\\'
    update_title()
  end
rescue => error
  @sequence_list.clear() # Errors during load invalidate the sequence
  Qt::MessageBox.critical(self, 'Error', error.message)
end

#file_save(save_as = false) ⇒ Object

Saves the GUI configuration to file. Also performs SaveAs by prompting for a new filename.

Parameters:

  • save_as (Boolean) (defaults to: false)

    Whether to SaveAs and prompt for a filename



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 329

def file_save(save_as = false)
  filename = @filename # Start with the current filename
  saved = false
  if filename == 'Untitled' # No file is currently open
    filename = Qt::FileDialog::getSaveFileName(self,         # parent
                                               'Save As...', # caption
                                               @sequence_dir + '/sequence.txt', # dir
                                               'Sequence Files (*.txt)') # filter
  elsif save_as
    filename = Qt::FileDialog::getSaveFileName(self,         # parent
                                               'Save As...', # caption
                                               filename,     # dir
                                               'Sequence Files (*.txt)') # filter
  end
  if !filename.nil? && !filename.empty?
    begin
      @sequence_list.save(filename)
      saved = true
      @filename = filename
      update_title()
      @sequence_dir = File.dirname(filename)
      @sequence_dir << '/' if @sequence_dir[-1..-1] != '/' and @sequence_dir[-1..-1] != '\\'
    rescue => error
      Qt::MessageBox.critical(self, 'Error', error.message)
    end
  end
  saved
end

#handle_pauseObject

Handles the pause button on the realtime_button_bar.



518
519
520
521
522
523
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 518

def handle_pause
  @pause = true
  @realtime_button_bar.state = 'Paused'
  @realtime_button_bar.start_button.setEnabled(true)
  output_append("User pressed Pause")
end

#handle_startObject

Handles the start button on the realtime_button_bar. This button changes as sequences are running to “Go” which skips any remaining wait time on the command. It also continues any paused sequences.



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 483

def handle_start
  case @realtime_button_bar.state
  when 'Stopped'
    return unless prompt_for_save_if_needed()
    # Collapse all items
    @sequence_list.map {|item| item.collapse }
    @pause = false
    @go = false
    @realtime_button_bar.state = 'Running'
    @realtime_button_bar.start_button.setText('Go')
    output_append("*** Sequence Started ***")
    @run_thread = Thread.new do
      @sequence_list.each do |item|
        Qt.execute_in_main_thread { @scroll.ensureWidgetVisible(item) }
        execute_item(item)
      end
      # Since we're inside a new Ruby thread
      Qt.execute_in_main_thread do
        output_append("*** Sequence Complete ***")
        @output.append("") # delimit the sequences in the output log
        @realtime_button_bar.start_button.setText('Start')
        @realtime_button_bar.state = 'Stopped'
      end
    end
  when 'Paused'
    output_append("User pressed #{@realtime_button_bar.start_button.text}")
    @realtime_button_bar.state = 'Running'
    @pause = false
  when 'Running'
    output_append("User pressed #{@realtime_button_bar.start_button.text}")
    @go = true
  end
end

#handle_stopObject

Handles the stop button on the realtime_button_bar. This kills the run_thread which requires the user to restart the sequence.



527
528
529
530
531
532
533
534
535
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 527

def handle_stop
  Cosmos.kill_thread(nil, @run_thread)
  @run_thread = nil
  @realtime_button_bar.start_button.setEnabled(true)
  @realtime_button_bar.start_button.setText('Start')
  @realtime_button_bar.state = 'Stopped'
  output_append("User pressed Stop")
  @output.append("") # delimit the sequences in the output log
end

#initialize_actionsObject

Creates the menu actions



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 111

def initialize_actions
  super()

  @file_new = Qt::Action.new(Cosmos.get_icon('file.png'), '&New', self)
  @file_new_keyseq = Qt::KeySequence.new('Ctrl+N')
  @file_new.shortcut  = @file_new_keyseq
  @file_new.statusTip = 'Start a new sequence'
  @file_new.connect(SIGNAL('triggered()')) { file_new() }

  @file_save = Qt::Action.new(Cosmos.get_icon('save.png'), '&Save', self)
  @file_save_keyseq = Qt::KeySequence.new('Ctrl+S')
  @file_save.shortcut  = @file_save_keyseq
  @file_save.statusTip = 'Save the sequence'
  @file_save.connect(SIGNAL('triggered()')) { file_save(false) }

  @file_save_as = Qt::Action.new(Cosmos.get_icon('save_as.png'), 'Save &As', self)
  @file_save_as.statusTip = 'Save the sequence'
  @file_save_as.connect(SIGNAL('triggered()')) { file_save(true) }

  @file_export = Qt::Action.new('&Export Sequence', self)
  @file_export.shortcut = Qt::KeySequence.new('Ctrl+E')
  @file_export.statusTip = 'Export the current sequence to a custom binary format'
  @file_export.connect(SIGNAL('triggered()')) { file_export() }

  @show_ignored = Qt::Action.new('&Show Ignored Parameters', self)
  @show_ignored.statusTip = 'Show ignored parameters which are normally hidden'
  @show_ignored.setCheckable(true)
  @show_ignored.setChecked(CmdParams.show_ignored)
  @show_ignored.connect(SIGNAL('toggled(bool)')) do |checked|
    # In case there aren't any sequences open, make sure to store the current value
    CmdParams.show_ignored = checked
    @sequence_list.map {|item| item.show_ignored(checked) }
  end

  @states_in_hex = Qt::Action.new('&Display State Values in Hex', self)
  @states_in_hex.statusTip = 'Display states values in hex instead of decimal'
  @states_in_hex.setCheckable(true)
  @states_in_hex.setChecked(CmdParams.states_in_hex)
  @states_in_hex.connect(SIGNAL('toggled(bool)')) do |checked|
    # In case there aren't any sequences open, make sure to store the current value
    CmdParams.states_in_hex = checked
    @sequence_list.map {|item| item.states_in_hex(checked) }
  end

  @expand_action = Qt::Action.new('&Expand All', self)
  @expand_action.statusTip = 'Expand all currently visible commands'
  @expand_action.connect(SIGNAL('triggered()')) do
    @sequence_list.map {|item| item.expand }
  end

  @collapse_action = Qt::Action.new('&Collapse All', self)
  @collapse_action.statusTip = 'Collapse all currently visible commands'
  @collapse_action.connect(SIGNAL('triggered()')) do
    @sequence_list.map {|item| item.collapse }
  end

  @script_disconnect = Qt::Action.new(Cosmos.get_icon('disconnected.png'), '&Toggle Disconnect', self)
  @script_disconnect_keyseq = Qt::KeySequence.new('Ctrl+T')
  @script_disconnect.shortcut  = @script_disconnect_keyseq
  @script_disconnect.statusTip = 'Toggle disconnect from the server'
  @script_disconnect.connect(SIGNAL('triggered()')) do
    @server_config_file ||= CmdTlmServer::DEFAULT_CONFIG_FILE
    @server_config_file = toggle_disconnect(@server_config_file)
  end
end

#initialize_central_widgetObject

Create the CmdSequence GUI



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 214

def initialize_central_widget
  central_widget = Qt::Widget.new
  setCentralWidget(central_widget)
  central_layout = Qt::VBoxLayout.new
  central_widget.layout = central_layout

  @realtime_button_bar = RealtimeButtonBar.new(self)
  @realtime_button_bar.start_callback = method(:handle_start)
  @realtime_button_bar.pause_callback = method(:handle_pause)
  @realtime_button_bar.stop_callback  = method(:handle_stop)
  @realtime_button_bar.state = 'Stopped'
  central_layout.addWidget(@realtime_button_bar)

  # Mnemonic Search Box
  @search_box = FullTextSearchLineEdit.new(self)
  central_layout.addWidget(@search_box)

  @target_select = Qt::ComboBox.new
  @target_select.setMaxVisibleItems(6)
  @target_select.connect(SIGNAL('activated(const QString&)')) {|target| target_changed() }
  target_label = Qt::Label.new("&Target:")
  target_label.setBuddy(@target_select)

  @cmd_select = Qt::ComboBox.new
  @cmd_select.setMaxVisibleItems(20)
  cmd_label = Qt::Label.new("&Command:")
  cmd_label.setBuddy(@cmd_select)

  add = Qt::PushButton.new("Add")
  add.connect(SIGNAL('clicked()')) { add_command() }

  # Layout the target and command selection with Add button
  select_layout = Qt::HBoxLayout.new
  select_layout.addWidget(target_label)
  select_layout.addWidget(@target_select, 1)
  select_layout.addWidget(cmd_label)
  select_layout.addWidget(@cmd_select, 1)
  select_layout.addWidget(add)
  central_layout.addLayout(select_layout)

  # Create a splitter to hold the sequence area and the script output text area
  splitter = Qt::Splitter.new(Qt::Vertical, self)
  central_layout.addWidget(splitter)

  @sequence_list = SequenceList.new(self)
  @sequence_list.connect(SIGNAL("modified()")) { update_title }

  @scroll = Qt::ScrollArea.new()
  @scroll.setSizePolicy(Qt::SizePolicy::Preferred, Qt::SizePolicy::Expanding)
  @scroll.setWidgetResizable(true)
  @scroll.setWidget(@sequence_list)
  connect(@scroll.verticalScrollBar(), SIGNAL("valueChanged(int)"), @sequence_list, SLOT("update()"))
  splitter.addWidget(@scroll)

  bottom_frame = Qt::Widget.new
  bottom_layout = Qt::VBoxLayout.new
  bottom_layout.setContentsMargins(0,0,0,0)
  bottom_layout_label = Qt::Label.new("Sequence Output:")
  bottom_layout.addWidget(bottom_layout_label)
  @output = Qt::TextEdit.new
  @output.setReadOnly(true)
  bottom_layout.addWidget(@output)
  bottom_frame.setLayout(bottom_layout)
  splitter.addWidget(bottom_frame)
  splitter.setStretchFactor(0,1)
  splitter.setStretchFactor(1,0)
end

#initialize_menusObject

Create the application menus and assign the actions



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 178

def initialize_menus
  file_menu = menuBar.addMenu('&File')
  file_menu.addAction(@file_new)

  open_action = Qt::Action.new(self)
  open_action.shortcut = Qt::KeySequence.new('Ctrl+O')
  open_action.connect(SIGNAL('triggered()')) { file_open(@sequence_dir) }
  self.addAction(open_action)

  file_open = file_menu.addMenu('&Open')
  file_open.setIcon(Cosmos.get_icon('open.png'))
  target_dirs_action(file_open, System.paths['SEQUENCES'], 'sequences', method(:file_open))

  file_menu.addAction(@file_save)
  file_menu.addAction(@file_save_as)
  if @exporter
    file_menu.addSeparator()
    file_menu.addAction(@file_export)
  end
  file_menu.addSeparator()
  file_menu.addAction(@exit_action)

  action_menu = menuBar.addMenu('&Actions')
  action_menu.addAction(@show_ignored)
  action_menu.addAction(@states_in_hex)
  action_menu.addSeparator()
  action_menu.addAction(@expand_action)
  action_menu.addAction(@collapse_action)
  action_menu.addSeparator()
  action_menu.addAction(@script_disconnect)

  @about_string = "Sequence Generator generates and executes sequences of commands."
  initialize_help_menu()
end

#toggle_disconnect(config_file, ask_for_config_file = true) ⇒ Object

Toggles whether CmdSequence is sending files to the CmdTlmServer (default) or disconnects and processes them all internally. The disconnected mode sets the background color to red to visually distinguish that no commands are actually going to the server.

Parameters:

  • config_file (String)

    cmd_tlm_server.txt configuration file to process when creating the disconnected server



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/cosmos/tools/cmd_sequence/cmd_sequence.rb', line 364

def toggle_disconnect(config_file, ask_for_config_file = true)
  dialog = Qt::Dialog.new(self, Qt::WindowTitleHint | Qt::WindowSystemMenuHint)
  dialog.setWindowTitle("Disconnect Settings")
  dialog_layout = Qt::VBoxLayout.new
  dialog_layout.addWidget(Qt::Label.new("Targets checked will be disconnected."))

  all_targets = {}
  set_clear_layout = Qt::HBoxLayout.new
  check_all = Qt::PushButton.new("Check All")
  check_all.setAutoDefault(false)
  check_all.setDefault(false)
  check_all.connect(SIGNAL('clicked()')) do
    all_targets.each do |target, checkbox|
      checkbox.setChecked(true)
    end
  end
  set_clear_layout.addWidget(check_all)
  clear_all = Qt::PushButton.new("Clear All")
  clear_all.connect(SIGNAL('clicked()')) do
    all_targets.each do |target, checkbox|
      checkbox.setChecked(false)
    end
  end
  set_clear_layout.addWidget(clear_all)
  dialog_layout.addLayout(set_clear_layout)

  scroll = Qt::ScrollArea.new
  target_widget = Qt::Widget.new
  scroll.setWidget(target_widget)
  target_layout = Qt::VBoxLayout.new(target_widget)
  target_layout.setSizeConstraint(Qt::Layout::SetMinAndMaxSize)
  scroll.setSizePolicy(Qt::SizePolicy::Preferred, Qt::SizePolicy::Expanding)
  scroll.setWidgetResizable(true)

  existing = get_disconnected_targets()
  System.targets.keys.each do |target|
    check_layout = Qt::HBoxLayout.new
    check_label = Qt::CheckboxLabel.new(target)
    checkbox = Qt::CheckBox.new
    all_targets[target] = checkbox
    if existing
      checkbox.setChecked(existing && existing.include?(target))
    else
      checkbox.setChecked(true)
    end
    check_label.setCheckbox(checkbox)
    check_layout.addWidget(checkbox)
    check_layout.addWidget(check_label)
    check_layout.addStretch
    target_layout.addLayout(check_layout)
  end
  dialog_layout.addWidget(scroll)

  if ask_for_config_file
    chooser = FileChooser.new(self, "Config File", config_file, 'Select',
                              File.dirname(config_file))
    chooser.callback = lambda do |filename|
      chooser.filename = filename
    end
    dialog_layout.addWidget(chooser)
  end

  button_layout = Qt::HBoxLayout.new
  ok = Qt::PushButton.new("Ok")
  ok.setAutoDefault(true)
  ok.setDefault(true)
  targets = []
  ok.connect(SIGNAL('clicked()')) do
    all_targets.each do |target, checkbox|
      targets << target if checkbox.isChecked
    end
    dialog.accept()
  end
  button_layout.addWidget(ok)
  cancel = Qt::PushButton.new("Cancel")
  cancel.connect(SIGNAL('clicked()')) do
    dialog.reject()
  end
  button_layout.addWidget(cancel)
  dialog_layout.addLayout(button_layout)

  dialog.setLayout(dialog_layout)
  if dialog.exec == Qt::Dialog::Accepted
    if targets.empty?
      clear_disconnected_targets()
      statusBar.showMessage("")
    else
      config_file = chooser.filename
      statusBar.showMessage("Targets disconnected: #{targets.join(" ")}")
      Splash.execute(self) do |splash|
        ConfigParser.splash = splash
        splash.message = "Initializing Command and Telemetry Server"
        set_disconnected_targets(targets, targets.length == all_targets.length, config_file)
        ConfigParser.splash = nil
      end
    end
  end
  dialog.dispose
  config_file
end