OpenC3/cosmos

View on GitHub
openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/CommandSender.vue

Summary

Maintainability
Test Coverage
<!--
# Copyright 2022 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 Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# Modified by OpenC3, Inc.
# All changes Copyright 2024, OpenC3, Inc.
# All Rights Reserved
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.
-->

<template>
  <div>
    <top-bar :menus="menus" :title="title" />
    <v-card
      ><div style="padding: 10px">
        <target-packet-item-chooser
          :initial-target-name="this.$route.params.target"
          :initial-packet-name="this.$route.params.packet"
          @on-set="commandChanged($event)"
          @click="buildCmd($event)"
          :disabled="sendDisabled"
          button-text="Send"
          mode="cmd"
        />
      </div>

      <v-card v-if="rows.length !== 0">
        <v-card-title>
          Parameters
          <v-spacer />
          <v-text-field
            v-model="search"
            label="Search"
            prepend-inner-icon="mdi-magnify"
            clearable
            outlined
            dense
            single-line
            hide-details
            class="search"
          />
        </v-card-title>
        <v-data-table
          :headers="headers"
          :items="rows"
          :search="search"
          calculate-widths
          disable-pagination
          hide-default-footer
          multi-sort
          dense
          @contextmenu:row="showContextMenu"
        >
          <template v-slot:item.val_and_states="{ item }">
            <command-parameter-editor
              v-model="item.val_and_states"
              :states-in-hex="statesInHex"
            />
          </template>
        </v-data-table>
      </v-card>
      <div class="pa-3">Status: {{ status }}</div>
    </v-card>
    <div style="height: 15px" />
    <multipane
      class="horizontal-panes"
      layout="horizontal"
      @paneResize="editor.resize()"
    >
      <v-row>
        <v-col>
          <v-card class="pb-2">
            <v-card-subtitle>
              Editable Command History: (Pressing Enter on the line re-executes
              the command)
              <v-tooltip top>
                <template v-slot:activator="{ on, attrs }">
                  <div v-on="on" v-bind="attrs" class="float-right">
                    <v-btn icon data-test="clear-history" @click="clearHistory">
                      <v-icon> mdi-delete </v-icon>
                    </v-btn>
                  </div>
                </template>
                <span> Clear History </span>
              </v-tooltip>
            </v-card-subtitle>
            <v-row class="mt-2 mb-2">
              <pre ref="editor" class="editor" data-test="sender-history"></pre>
            </v-row>
          </v-card>
        </v-col>
        <v-col v-if="screenDefinition" md="auto">
          <openc3-screen
            v-if="screenDefinition"
            :target="screenTarget"
            :screen="screenName"
            :definition="screenDefinition"
            :keywords="keywords"
            :count="screenCount"
            :showClose="false"
          />
        </v-col>
      </v-row>
    </multipane>
    <div style="height: 15px" />

    <v-menu
      v-model="contextMenuShown"
      :position-x="x"
      :position-y="y"
      absolute
      offset-y
    >
      <v-list>
        <v-list-item
          v-for="(item, index) in contextMenuOptions"
          :key="index"
          @click.stop="item.action"
        >
          <v-list-item-title>{{ item.title }}</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>
    <details-dialog
      :target-name="targetName"
      :packet-name="commandName"
      :item-name="parameterName"
      :type="'cmd'"
      v-model="viewDetails"
    />
    <critical-cmd-dialog
      :uuid="criticalCmdUuid"
      :cmdString="criticalCmdString"
      :cmdUser="criticalCmdUser"
      v-model="displayCriticalCmd"
    />

    <v-dialog v-model="displayErrorDialog" max-width="600">
      <v-card>
        <v-system-bar>
          <v-spacer />
          <span> Error </span>
          <v-spacer />
        </v-system-bar>
        <v-card-text>
          <div class="mx-1">
            <v-row class="my-2">
              <v-card-text>
                <span v-text="status" />
              </v-card-text>
            </v-row>
            <v-row>
              <v-spacer />
              <v-btn
                @click="displayErrorDialog = false"
                color="primary"
                data-test="error-dialog-ok"
              >
                Ok
              </v-btn>
            </v-row>
          </div>
        </v-card-text>
      </v-card>
    </v-dialog>

    <v-dialog v-model="displaySendHazardous" max-width="600">
      <v-card>
        <v-system-bar>
          <v-spacer />
          <span> Hazardous Warning </span>
          <v-spacer />
        </v-system-bar>
        <v-card-text>
          <div class="mx-1">
            <v-row class="my-2">
              <span>
                Warning: Command {{ hazardousCommand }} is Hazardous. Send?
              </span>
            </v-row>
            <v-row>
              <v-spacer />
              <v-btn @click="cancelHazardousCmd" outlined> Cancel </v-btn>
              <v-btn @click="sendHazardousCmd" class="primary mx-1">
                Send
              </v-btn>
            </v-row>
          </div>
        </v-card-text>
      </v-card>
    </v-dialog>

    <v-dialog v-model="displaySendRaw" max-width="600">
      <v-card>
        <v-system-bar>
          <v-spacer />
          <span> Send Raw </span>
          <v-spacer />
        </v-system-bar>
        <v-card-text>
          <div class="mx-1">
            <v-row class="my-2">
              <v-col>Interface:</v-col>
              <v-col>
                <v-select
                  solo
                  hide-details
                  dense
                  :items="interfaces"
                  item-text="label"
                  item-value="value"
                  v-model="selectedInterface"
                />
              </v-col>
            </v-row>
            <v-row no-gutters>
              <v-col>Filename:</v-col>
              <v-col>
                <input type="file" @change="selectRawCmdFile($event)" />
              </v-col>
            </v-row>
            <v-row>
              <v-spacer />
              <v-btn @click="cancelRawCmd" outlined data-test="raw-cancel">
                Cancel
              </v-btn>
              <v-btn @click="sendRawCmd" class="primary" data-test="raw-ok">
                Ok
              </v-btn>
            </v-row>
          </div>
        </v-card-text>
      </v-card>
    </v-dialog>
  </div>
</template>

<script>
import * as ace from 'ace-builds'
import 'ace-builds/src-min-noconflict/mode-ruby'
import 'ace-builds/src-min-noconflict/theme-twilight'
import Api from '@openc3/tool-common/src/services/api'
import TargetPacketItemChooser from '@openc3/tool-common/src/components/TargetPacketItemChooser'
import CommandParameterEditor from '@/tools/CommandSender/CommandParameterEditor'
import Utilities from '@/tools/CommandSender/utilities'
import { OpenC3Api } from '@openc3/tool-common/src/services/openc3-api'
import DetailsDialog from '@openc3/tool-common/src/components/DetailsDialog'
import CriticalCmdDialog from '@openc3/tool-common/src/components/CriticalCmdDialog'
import TopBar from '@openc3/tool-common/src/components/TopBar'
import Openc3Screen from '@openc3/tool-common/src/components/Openc3Screen'
import 'sprintf-js'

export default {
  mixins: [Utilities],
  components: {
    DetailsDialog,
    CriticalCmdDialog,
    TargetPacketItemChooser,
    CommandParameterEditor,
    TopBar,
    Openc3Screen,
  },
  data() {
    return {
      title: 'Command Sender',
      search: '',
      headers: [
        { text: 'Name', value: 'parameter_name' },
        { text: 'Value or State', value: 'val_and_states' },
        { text: 'Units', value: 'units' },
        { text: 'Range', value: 'range' },
        { text: 'Description', value: 'description' },
      ],
      editor: null,
      targetName: '',
      commandName: '',
      paramList: '',
      lastTargetName: '',
      lastCommandName: '',
      lastParamList: '',
      ignoreRangeChecks: false,
      statesInHex: false,
      showIgnoredParams: false,
      cmdRaw: false,
      ignoredParams: [],
      rows: [],
      interfaces: [],
      selectedInterface: '',
      rawCmdFile: null,
      status: '',
      history: '',
      hazardousCommand: '',
      displaySendHazardous: false,
      displayErrorDialog: false,
      displaySendRaw: false,
      displayCriticalCmd: false,
      sendDisabled: false,
      api: null,
      viewDetails: false,
      contextMenuShown: false,
      parameterName: '',
      reservedItemNames: [],
      x: 0,
      y: 0,
      contextMenuOptions: [
        {
          title: 'Details',
          action: () => {
            this.contextMenuShown = false
            this.viewDetails = true
          },
        },
      ],
      keywords: [],
      screenTarget: null,
      screenName: null,
      screenDefinition: null,
      screenCount: 0,
      menus: [
        // TODO: Implement send raw
        // {
        //   label: 'File',
        //   items: [
        //     {
        //       label: 'Send Raw',
        //       command: () => {
        //         this.setupRawCmd()
        //       }
        //     }
        //   ]
        // },
        {
          label: 'Mode',
          items: [
            {
              label: 'Ignore Range Checks',
              checkbox: true,
              command: () => {
                this.ignoreRangeChecks = !this.ignoreRangeChecks
              },
            },
            {
              label: 'Display State Values in Hex',
              checkbox: true,
              command: () => {
                this.statesInHex = !this.statesInHex
              },
            },
            {
              label: 'Show Ignored Parameters',
              checkbox: true,
              command: () => {
                this.showIgnoredParams = !this.showIgnoredParams
                // TODO: Maybe we don't need to do this if the data-table
                // can render the whole thing and we just display with v-if
                this.updateCmdParams()
              },
            },
            {
              label: 'Disable Parameter Conversions',
              checkbox: true,
              command: () => {
                this.cmdRaw = !this.cmdRaw
              },
            },
          ],
        },
      ],
    }
  },
  created() {
    Api.get(`/openc3-api/autocomplete/reserved-item-names`).then((response) => {
      this.reservedItemNames = response.data
    })
    this.api = new OpenC3Api()
    // If we're passed in the route then manually call commandChanged to update
    if (this.$route.params.target && this.$route.params.packet) {
      this.commandChanged({
        targetName: this.$route.params.target.toUpperCase(),
        packetName: this.$route.params.packet.toUpperCase(),
      })
    }
    Api.get('/openc3-api/autocomplete/keywords/screen').then((response) => {
      this.keywords = response.data
    })
  },
  mounted() {
    this.editor = ace.edit(this.$refs.editor)
    this.editor.setTheme('ace/theme/twilight')
    this.editor.session.setMode('ace/mode/ruby')
    this.editor.session.setTabSize(2)
    this.editor.session.setUseWrapMode(true)
    this.editor.setHighlightActiveLine(false)
    this.editor.setValue(localStorage['command_sender__history'])
    this.history = this.editor.getValue().trim()
    this.editor.clearSelection()
    this.editor.focus()
    this.editor.setAutoScrollEditorIntoView(true)
    // This only limits the displayed lines, history can grow in a scrollable window
    this.editor.setOption('maxLines', 30)
    this.editor.setOption('minLines', 1)
    this.editor.container.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') {
        e.preventDefault()
        let command = this.editor.session.getLine(
          this.editor.getCursorPosition().row,
        )
        // Blank commands can happen if typing return on a blank line
        if (command === '') {
          return
        }
        // Remove the cmd("") wrapper
        let firstQuote = command.indexOf('"')
        let lastQuote = command.lastIndexOf('"')
        command = command.substr(firstQuote + 1, lastQuote - firstQuote - 1)
        this.sendCmd(command)
      }
    })
  },
  beforeDestroy() {
    this.editor.destroy()
    this.editor.container.remove()
  },
  methods: {
    showContextMenu(e, row) {
      e.preventDefault()
      this.parameterName = row.item.parameter_name
      this.contextMenuShown = false
      this.x = e.clientX
      this.y = e.clientY
      this.$nextTick(() => {
        this.contextMenuShown = true
      })
    },
    convertToValue(param) {
      if (
        param.val_and_states.selected_state !== null &&
        param.val_and_states.selected_state !== 'MANUALLY ENTERED' &&
        this.cmdRaw === false
      ) {
        return param.val_and_states.selected_state_label
      }
      if (typeof param.val_and_states.val !== 'string') {
        return param.val_and_states.val
      }

      var str = param.val_and_states.val
      var quotesRemoved = this.removeQuotes(str)
      if (str === quotesRemoved) {
        var upcaseStr = str.toUpperCase()
        if (
          (param.type === 'STRING' || param.type === 'BLOCK') &&
          upcaseStr.startsWith('0X')
        ) {
          var hexStr = upcaseStr.slice(2)
          if (hexStr.length % 2 !== 0) {
            hexStr = '0' + hexStr
          }
          var jstr = { json_class: 'String', raw: [] }
          for (var i = 0; i < hexStr.length; i += 2) {
            var nibble = hexStr.charAt(i) + hexStr.charAt(i + 1)
            jstr.raw.push(parseInt(nibble, 16))
          }
          return jstr
        } else {
          if (upcaseStr === 'INFINITY') {
            return Infinity
          } else if (upcaseStr === '-INFINITY') {
            return -Infinity
          } else if (upcaseStr === 'NAN') {
            return NaN
          } else if (this.isFloat(str)) {
            return parseFloat(str)
          } else if (this.isInt(str)) {
            return parseInt(str)
          } else if (this.isArray(str)) {
            return eval(str)
          } else {
            return str
          }
        }
      } else {
        return quotesRemoved
      }
    },

    commandChanged(event) {
      if (
        this.targetName !== event.targetName ||
        this.commandName !== event.packetName
      ) {
        this.targetName = event.targetName
        this.commandName = event.packetName
        // Only updateCmdParams if we're not already in the middle of an update
        if (this.sendDisabled === false) {
          this.updateCmdParams()
        }
        this.$router
          .replace({
            name: 'CommandSender',
            params: {
              target: this.targetName,
              packet: this.commandName,
            },
          })
          // catch the error in case we route to where we already are
          .catch((err) => {})
      }
    },

    updateCmdParams() {
      this.sendDisabled = true
      this.ignoredParams = []
      this.rows = []
      this.api
        .get_target(this.targetName)
        .then(
          (target) => {
            this.ignoredParams = target.ignored_parameters
            return this.api.get_cmd(this.targetName, this.commandName)
          },
          (error) => {
            this.displayError('getting ignored parameters', error)
            this.sendDisabled = false
          },
        )
        .then(
          (command) => {
            command.items.forEach((parameter) => {
              if (this.reservedItemNames.includes(parameter.name)) return
              if (
                !this.ignoredParams.includes(parameter.name) ||
                this.showIgnoredParams
              ) {
                let val = parameter.default
                // If the parameter is a string and the default is a string
                // (rather than object for binary) then we quote the string
                // However we don't do this is the parameter has states
                // because that messes up the state selection logic
                if (
                  !parameter.states &&
                  parameter.data_type === 'STRING' &&
                  typeof parameter.default === 'string'
                ) {
                  val = `'${val}'`
                }
                if (parameter.required) {
                  val = ''
                }
                if (parameter.format_string) {
                  val = sprintf(parameter.format_string, parameter.default)
                }
                let range = 'N/A'
                // check using != because compare with null
                if (parameter.minimum != null && parameter.maximum != null) {
                  if (parameter.data_type === 'FLOAT') {
                    // This is basically to handle the FLOAT MIN and MAX so they
                    // don't print out the huge exponential
                    if (parameter.minimum < -1e6) {
                      parameter.minimum = parameter.minimum.toExponential(3)
                    }
                    if (parameter.maximum > 1e6) {
                      parameter.maximum = parameter.maximum.toExponential(3)
                    }
                  }
                  range = `${parameter.minimum}..${parameter.maximum}`
                }
                this.rows.push({
                  parameter_name: parameter.name,
                  val_and_states: {
                    val: val,
                    states: parameter.states,
                  },
                  description: parameter.description,
                  range: range,
                  units: parameter.units,
                  type: parameter.data_type,
                })
              }
            })
            if (command.screen) {
              this.loadScreen(command.screen[0], command.screen[1]).then(
                (response) => {
                  this.screenTarget = command.screen[0]
                  this.screenName = command.screen[1]
                  this.screenDefinition = response.data
                  this.screenCount += 1
                },
              )
            } else {
              if (command.related_items) {
                this.screenTarget = 'LOCAL'
                this.screenName = 'CMDSENDER'
                let screenDefinition = 'SCREEN AUTO AUTO 1.0\n'
                for (var i = 0; i < command.related_items.length; i++) {
                  screenDefinition += `LABELVALUE '${command.related_items[i][0]}' '${command.related_items[i][1]}' '${command.related_items[i][2]}' WITH_UNITS 20\n`
                }
                this.screenDefinition = screenDefinition
              } else {
                this.screenTarget = null
                this.screenName = null
                this.screenDefinition = null
              }
              this.screenCount += 1
            }
            this.sendDisabled = false
            this.status = ''
          },
          (error) => {
            this.displayError('getting command parameters', error)
            this.sendDisabled = false
          },
        )
    },

    createParamList() {
      let paramList = {}
      for (var i = 0; i < this.rows.length; i++) {
        paramList[this.rows[i].parameter_name] = this.convertToValue(
          this.rows[i],
        )
      }
      return paramList
    },

    buildCmd() {
      this.sendCmd(this.targetName, this.commandName, this.createParamList())
    },

    // Note targetName can also be the entire command to send, e.g. "INST ABORT" or
    // "INST COLLECT with TYPE 0, DURATION 1, OPCODE 171, TEMP 10" when being
    // sent from the history. In that case commandName and paramList are undefined
    // and the api calls handle that.
    sendCmd(targetName, commandName, paramList) {
      // Store what was actually sent for use in resending hazardous commands
      this.lastTargetName = targetName
      this.lastCommandName = commandName
      this.lastParamList = paramList

      this.sendDisabled = true
      let hazardous = false
      let cmd = ''
      this.api.get_cmd_hazardous(targetName, commandName, paramList).then(
        (response) => {
          hazardous = response

          if (hazardous) {
            // If it was sent from history it's all in targetName
            if (commandName === undefined) {
              this.hazardousCommand = targetName
                .split(' ')
                .slice(0, 2)
                .join(' ')
            } else {
              this.hazardousCommand = `${targetName} ${commandName}`
            }
            this.displaySendHazardous = true
          } else {
            let obs
            if (this.cmdRaw) {
              if (this.ignoreRangeChecks) {
                cmd = 'cmd_raw_no_range_check'
                obs = this.api.cmd_raw_no_range_check(
                  targetName,
                  commandName,
                  paramList,
                  {
                    'Ignore-Errors': '428',
                  },
                )
              } else {
                cmd = 'cmd_raw'
                obs = this.api.cmd_raw(targetName, commandName, paramList, {
                  // This request could be denied due to out of range but since
                  // we're explicitly handling it we don't want the interceptor to fire
                  'Ignore-Errors': '428 500',
                })
              }
            } else {
              if (this.ignoreRangeChecks) {
                cmd = 'cmd_no_range_check'
                obs = this.api.cmd_no_range_check(
                  targetName,
                  commandName,
                  paramList,
                  {
                    'Ignore-Errors': '428',
                  },
                )
              } else {
                cmd = 'cmd'
                obs = this.api.cmd(targetName, commandName, paramList, {
                  // This request could be denied due to out of range but since
                  // we're explicitly handling it we don't want the interceptor to fire
                  'Ignore-Errors': '428 500',
                })
              }
            }

            obs.then(
              (response) => {
                this.processCmdResponse(
                  true,
                  targetName,
                  commandName,
                  cmd,
                  response,
                )
              },
              (error) => {
                this.processCmdResponse(
                  false,
                  targetName,
                  commandName,
                  cmd,
                  error,
                )
              },
            )
          }
        },
        (error) => {
          this.processCmdResponse(false, targetName, commandName, cmd, error)
        },
      )
    },

    sendHazardousCmd() {
      this.displaySendHazardous = false
      let obs = ''
      let cmd = ''
      if (this.cmdRaw) {
        if (this.ignoreRangeChecks) {
          cmd = 'cmd_raw_no_range_check'
          obs = this.api.cmd_raw_no_checks(
            this.lastTargetName,
            this.lastCommandName,
            this.lastParamList,
            {
              'Ignore-Errors': '428',
            },
          )
        } else {
          cmd = 'cmd_raw'
          obs = this.api.cmd_raw_no_hazardous_check(
            this.lastTargetName,
            this.lastCommandName,
            this.lastParamList,
            {
              // This request could be denied due to out of range but since
              // we're explicitly handling it we don't want the interceptor to fire
              'Ignore-Errors': '428 500',
            },
          )
        }
      } else {
        if (this.ignoreRangeChecks) {
          cmd = 'cmd_no_range_check'
          obs = this.api.cmd_no_checks(
            this.lastTargetName,
            this.lastCommandName,
            this.lastParamList,
            {
              'Ignore-Errors': '428',
            },
          )
        } else {
          cmd = 'cmd'
          obs = this.api.cmd_no_hazardous_check(
            this.lastTargetName,
            this.lastCommandName,
            this.lastParamList,
            {
              // This request could be denied due to out of range but since
              // we're explicitly handling it we don't want the interceptor to fire
              'Ignore-Errors': '428 500',
            },
          )
        }
      }

      obs.then(
        (response) => {
          this.processCmdResponse(
            true,
            this.lastTargetName,
            this.lastCommandName,
            cmd,
            response,
          )
        },
        (error) => {
          this.processCmdResponse(
            false,
            this.lastTargetName,
            this.lastCommandName,
            cmd,
            error,
          )
        },
      )
    },

    cancelHazardousCmd() {
      this.displaySendHazardous = false
      this.status = 'Hazardous command not sent'
      this.sendDisabled = false
    },

    processCmdResponse(success, targetName, commandName, cmd_sent, response) {
      // If it was sent from history it's all in targetName, see sendCmd for details
      if (commandName === undefined) {
        ;[targetName, commandName] = targetName.split(' ').slice(0, 2)
      }
      var msg = ''
      if (success) {
        msg = `${cmd_sent}("${response[0]} ${response[1]}`
        var keys = Object.keys(response[2])
        if (keys.length > 0) {
          msg += ' with '
          for (var i = 0; i < keys.length; i++) {
            var key = keys[i]
            var value = this.convertToString(response[2][key])
            // If the response has unquoted string data we add quotes
            if (
              typeof response[2][key] === 'string' &&
              value.charAt(0) !== "'" &&
              value.charAt(0) !== '"'
            ) {
              value = `'${value}'`
            }
            msg += key + ' ' + value
            if (i < keys.length - 1) {
              msg += ', '
            }
          }
        }
        msg += '")'
        if (!this.history.includes(msg)) {
          value = msg
          if (this.history.length !== 0) {
            value += `\n${this.history}`
          }
          this.editor.setValue(value)
          this.editor.moveCursorTo(0, 0)
        }
        msg += ' sent.'
        // Add the number of commands sent to the status message
        if (this.status.includes(msg)) {
          let parts = this.status.split('sent.')
          if (parts[1].includes('(')) {
            let num = parseInt(parts[1].substr(2, parts[1].indexOf(')') - 2))
            msg = parts[0] + 'sent. (' + (num + 1) + ')'
          } else {
            msg += ' (2)'
          }
        }
        this.status = msg
      } else {
        var context = 'sending ' + targetName + ' ' + commandName
        this.displayError(context, response, true)
      }
      // Make a copy of the history
      this.history = this.editor.getValue()
      localStorage['command_sender__history'] = this.editor.getValue()
      this.sendDisabled = false
    },

    clearHistory() {
      this.editor.setValue('')
      this.history = ''
      localStorage.removeItem('command_sender__history')
    },

    displayError(context, error, showDialog = false) {
      this.status = `Error ${context} due to ${error.name}`
      if (error.message && error.message !== '') {
        this.status += ': '
        this.status += error.message
      }
      if (this.status.includes('CriticalCmdError')) {
        this.status = `Critical Command Queued For Approval`
      }
      if (showDialog) {
        if (error.message.includes('CriticalCmdError')) {
          this.criticalCmdUuid = error.object.data.instance_variables['@uuid']
          this.criticalCmdString =
            error.object.data.instance_variables['@cmd_string']
          this.criticalCmdUser =
            error.object.data.instance_variables['@username']
          this.displayCriticalCmd = true
        } else {
          this.displayErrorDialog = true
        }
      }
    },

    loadScreen(target, screen) {
      return Api.get('/openc3-api/screen/' + target + '/' + screen, {
        headers: {
          Accept: 'text/plain',
        },
      })
    },

    // setupRawCmd() {
    //   this.api.get_interface_names().then(
    //     (response) => {
    //       var interfaces = []
    //       for (var i = 0; i < response.length; i++) {
    //         interfaces.push({ label: response[i], value: response[i] })
    //       }
    //       this.interfaces = interfaces
    //       this.selectedInterface = interfaces[0].value
    //       this.displaySendRaw = true
    //     },
    //     (error) => {
    //       this.displaySendRaw = false
    //       this.displayError('getting interface names', error, true)
    //     }
    //   )
    // },

    // selectRawCmdFile(event) {
    //   this.rawCmdFile = event.target.files[0]
    // },

    // onLoad(event) {
    //   var bufView = new Uint8Array(event.target.result)
    //   var jstr = { json_class: 'String', raw: [] }
    //   for (var i = 0; i < bufView.length; i++) {
    //     jstr.raw.push(bufView[i])
    //   }

    //   this.api.send_raw(this.selectedInterface, jstr).then(
    //     () => {
    //       this.displaySendRaw = false
    //       this.status =
    //         'Sent ' +
    //         bufView.length +
    //         ' bytes to interface ' +
    //         this.selectedInterface
    //     },
    //     (error) => {
    //       this.displaySendRaw = false
    //       this.displayError('sending raw data', error, true)
    //     }
    //   )
    // },

    // sendRawCmd() {
    //   var self = this
    //   var reader = new FileReader()
    //   reader.onload = function (e) {
    //     self.onLoad(e)
    //   }
    //   reader.onerror = function (e) {
    //     self.displaySendRaw = false
    //     var target = e.target
    //     self.displayError('sending raw data', target.error, true)
    //   }
    //   // TBD - use the other event handlers to implement a progress bar for the
    //   // file upload.  Handle abort as well?
    //   //reader.onloadstart = function(e) {}
    //   //reader.onprogress = function(e) {}
    //   //reader.onloadend = function(e) {}
    //   //reader.onabort = function(e) {}

    //   reader.readAsArrayBuffer(this.rawCmdFile)
    // },

    // cancelRawCmd() {
    //   this.displaySendRaw = false
    //   this.status = 'Raw command not sent'
    // },
  },
}
</script>
<style scoped>
.editor {
  margin-left: 30px;
  height: 50px;
  width: 95%;
  position: relative;
  font-size: 16px;
}
</style>