OpenC3/cosmos

View on GitHub
openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdtlmserver/src/tools/CmdTlmServer/RawDialog.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 2023, 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
    v-show="isVisible"
    :style="computedStyle"
    class="raw-dialog"
    ref="rawDialog"
  >
    <v-card>
      <div ref="bar">
        <v-system-bar>
          <v-tooltip top>
            <template v-slot:activator="{ on, attrs }">
              <div v-on="on" v-bind="attrs">
                <v-icon data-test="copy-icon" @click="copyRawData">
                  mdi-content-copy
                </v-icon>
              </div>
            </template>
            <span> Copy </span>
          </v-tooltip>
          <v-tooltip top>
            <template v-slot:activator="{ on, attrs }">
              <div v-on="on" v-bind="attrs">
                <v-icon data-test="download" @click="downloadRawData">
                  mdi-download
                </v-icon>
              </div>
            </template>
            <span> Download </span>
          </v-tooltip>
          <v-spacer />
          <span> {{ type }} </span>
          <v-spacer />
          <v-tooltip top>
            <template v-slot:activator="{ on, attrs }">
              <div v-on="on" v-bind="attrs">
                <v-icon data-test="close" @click="$emit('close')">
                  mdi-close-box
                </v-icon>
              </div>
            </template>
            <span> Close </span>
          </v-tooltip>
        </v-system-bar>
      </div>
      <v-card-title>
        <span> {{ header }} </span>
        <v-spacer />
        <v-tooltip top>
          <template v-slot:activator="{ on, attrs }">
            <div v-on="on" v-bind="attrs">
              <v-btn icon data-test="pause" @click="pause">
                <v-icon> {{ buttonIcon }} </v-icon>
              </v-btn>
            </div>
          </template>
          <span> {{ buttonLabel }} </span>
        </v-tooltip>
      </v-card-title>
      <v-card-text>
        <v-row dense>
          <v-col cols="4">
            <span> Received Time: </span>
          </v-col>
          <v-col class="text-right">
            <span> {{ receivedTime }} </span>
          </v-col>
        </v-row>
        <v-row dense>
          <v-col cols="4">
            <span> Count: </span>
          </v-col>
          <v-col class="text-right">
            <span> {{ receivedCount }} </span>
          </v-col>
        </v-row>
        <v-textarea v-model="rawData" class="pa-0 ma-0" auto-grow readonly />
      </v-card-text>
    </v-card>
  </div>
</template>

<script>
import { format } from 'date-fns'

import Updater from './Updater'

export default {
  mixins: [Updater],
  props: {
    type: String,
    visible: Boolean,
    targetName: String,
    packetName: String,
    zIndex: {
      type: Number,
      default: 1,
    },
  },
  data() {
    return {
      header: '',
      receivedTime: '',
      rawData: '',
      paused: false,
      receivedCount: '',
      dragX: 0,
      dragY: 0,
      top: 0,
      left: 0,
    }
  },
  computed: {
    buttonLabel: function () {
      if (this.paused) {
        return 'Resume'
      } else {
        return 'Pause'
      }
    },
    buttonIcon: function () {
      if (this.paused) {
        return 'mdi-play'
      } else {
        return 'mdi-pause'
      }
    },
    isVisible: {
      get: function () {
        return this.visible
      },
      // Reset all the data to defaults
      set: function (bool) {
        this.header = ''
        this.receivedTime = ''
        this.rawData = ''
        this.receivedCount = ''
        this.paused = false
        this.buttonLabel = 'Pause'
        this.$emit('display', bool)
      },
    },
    computedStyle() {
      let style = {}
      style['top'] = this.top + 'px'
      style['left'] = this.left + 'px'
      style['z-index'] = this.zIndex
      return style
    },
  },
  mounted() {
    this.$refs.bar.onmousedown = this.dragMouseDown
    this.$refs.rawDialog.onmouseup = this.focusEvent
  },
  methods: {
    focusEvent: function (e) {
      this.$emit('focus')
    },
    dragMouseDown: function (e) {
      e = e || window.event
      e.preventDefault()
      // get the mouse cursor position at startup:
      this.dragX = e.clientX
      this.dragY = e.clientY
      document.onmouseup = this.closeDragElement
      // call a function whenever the cursor moves:
      document.onmousemove = this.elementDrag
    },
    elementDrag: function (e) {
      e = e || window.event
      e.preventDefault()
      // calculate the new cursor position:
      let xOffset = this.dragX - e.clientX
      let yOffset = this.dragY - e.clientY
      this.dragX = e.clientX
      this.dragY = e.clientY
      // set the element's new position:
      this.top = this.$refs.bar.parentElement.parentElement.offsetTop - yOffset
      this.left =
        this.$refs.bar.parentElement.parentElement.offsetLeft - xOffset
    },
    closeDragElement: function () {
      // stop moving when mouse button is released
      document.onmouseup = null
      document.onmousemove = null
    },
    buildRawData: function () {
      return `${this.header}\nReceived Time: ${this.receivedTime}\nCount: ${this.receivedCount}\n${this.rawData}`
    },
    copyRawData: function () {
      navigator.clipboard.writeText(this.buildRawData())
    },
    downloadRawData: function () {
      const blob = new Blob([this.buildRawData()], {
        type: 'plain/text',
      })
      // Make a link and then 'click' on it to start the download
      const link = document.createElement('a')
      link.href = URL.createObjectURL(blob)
      const dt = format(Date.now(), 'yyyy_MM_dd_HH_mm_ss')
      link.setAttribute(
        'download',
        `${dt}_${this.targetName}_${this.packetName}.txt`,
      )
      link.click()
    },
    pause: function () {
      this.paused = !this.paused
    },
    update: function () {
      if (!this.isVisible || this.paused) return
      this.header = `Raw ${this.type} Packet: ${this.targetName} ${this.packetName}`

      if (this.type === 'Telemetry') {
        this.updateTelemetry()
      } else {
        this.updateCommand()
      }
    },
    updateTelemetry: function () {
      this.api
        .get_tlm_buffer(this.targetName, this.packetName)
        .then((result) => {
          let buffer_data = result.buffer
          if (buffer_data.raw !== undefined) {
            buffer_data = buffer_data.raw
          } else {
            let utf8Encode = new TextEncoder()
            buffer_data = utf8Encode.encode(buffer_data)
          }
          this.receivedTime = new Date(result.time / 1000000)
          this.receivedCount = result.received_count
          this.rawData =
            'Address   Data                                             Ascii\n' +
            '---------------------------------------------------------------------------\n' +
            this.formatBuffer(buffer_data)
        })
    },
    updateCommand: function () {
      this.api
        .get_cmd_buffer(this.targetName, this.packetName)
        .then((result) => {
          let buffer_data = result.buffer
          if (buffer_data.raw !== undefined) {
            buffer_data = buffer_data.raw
          } else {
            let utf8Encode = new TextEncoder()
            buffer_data = utf8Encode.encode(buffer_data)
          }
          this.receivedTime = new Date(result.time / 1000000)
          this.receivedCount = result.received_count
          this.rawData =
            'Address   Data                                             Ascii\n' +
            '---------------------------------------------------------------------------\n' +
            this.formatBuffer(buffer_data)
        })
    },
    // TODO: Perhaps move this to a utility library
    formatBuffer: function (buffer) {
      var string = ''
      var index = 0
      var ascii = ''
      buffer.forEach((byte) => {
        if (index % 16 === 0) {
          string += this.numHex(index, 8) + ': '
        }
        string += this.numHex(byte)

        // Create the ASCII representation if printable
        if (byte >= 32 && byte <= 126) {
          ascii += String.fromCharCode(byte)
        } else {
          ascii += ' '
        }

        index++

        if (index % 16 === 0) {
          string += '  ' + ascii + '\n'
          ascii = ''
        } else {
          string += ' '
        }
      })

      // We're done printing all the bytes. Now check to see if we ended in the
      // middle of a line. If so we have to print out the final ASCII if
      // requested.
      if (index % 16 != 0) {
        var existing_length = (index % 16) - 1 + (index % 16) * 2
        // 47 is (16 * 2) + 15 separator spaces
        var filler = ' '.repeat(47 - existing_length)
        var ascii_filler = ' '.repeat(16 - ascii.length)
        string += filler + '  ' + ascii + ascii_filler
      }
      return string
    },
    numHex(num, width = 2) {
      var hex = num.toString(16)
      return '0'.repeat(width - hex.length) + hex
    },
  },
}
</script>
<style scoped>
.raw-dialog {
  position: absolute;
  top: 0px;
  left: 5px;
  z-index: 1;
  border: solid;
  border-width: 1px;
  border-color: white;
  resize: both;
  overflow: auto;
  max-height: 85vh;
  background-color: var(--color-background-base-selected);
}
.raw-dialog :deep(.v-card) {
  height: 100%;
  min-width: 800px;
}
.raw-dialog :deep(.v-card__text) {
  height: 100%;
  background-color: var(--color-background-base-selected);
}
.v-textarea :deep(textarea) {
  margin-top: 10px;
  font-family: 'Courier New', Courier, monospace;
  overflow-y: scroll;
}
</style>