diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/CommandSender.vue b/openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/CommandSender.vue
index bcf8cab737..73e317d522 100644
--- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/CommandSender.vue
+++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-tool-cmdsender/src/tools/CommandSender/CommandSender.vue
@@ -176,7 +176,11 @@
Filename:
-
+
@@ -265,18 +269,17 @@ export default {
computed: {
menus: function () {
return [
- // TODO: Implement send raw
- // {
- // label: 'File',
- // items: [
- // {
- // label: 'Send Raw',
- // command: () => {
- // this.setupRawCmd()
- // },
- // },
- // ],
- // },
+ {
+ label: 'File',
+ items: [
+ {
+ label: 'Send Raw File',
+ command: () => {
+ this.setupRawCmd()
+ },
+ },
+ ],
+ },
{
label: 'Mode',
items: [
@@ -863,71 +866,71 @@ export default {
})
},
- // setupRawCmd() {
- // this.api.get_interface_names().then(
- // (response) => {
- // let interfaces = []
- // for (let 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) {
- // let bufView = new Uint8Array(event.target.result)
- // let jstr = { json_class: 'String', raw: [] }
- // for (let 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)
- // }
- // )
- // },
+ setupRawCmd() {
+ this.api.get_interface_names().then(
+ (response) => {
+ const interfaces = response.map((name) => ({
+ label: name,
+ value: name,
+ }))
+ this.interfaces = interfaces
+ this.selectedInterface = interfaces.length ? interfaces[0].value : ''
+ this.rawCmdFile = null
+ this.displaySendRaw = true
+ },
+ (error) => {
+ this.displaySendRaw = false
+ this.displayError('getting interface names', error, true)
+ },
+ )
+ },
- // sendRawCmd() {
- // let self = this
- // let reader = new FileReader()
- // reader.onload = function (e) {
- // self.onLoad(e)
- // }
- // reader.onerror = function (e) {
- // self.displaySendRaw = false
- // let 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) {}
+ selectRawCmdFile(event) {
+ this.rawCmdFile = event.target.files[0]
+ },
- // reader.readAsArrayBuffer(this.rawCmdFile)
- // },
+ async sendRawCmd() {
+ if (!this.selectedInterface) {
+ this.displayError(
+ 'sending raw data',
+ { name: 'Error', message: 'No interface selected' },
+ true,
+ )
+ return
+ }
+ if (!this.rawCmdFile) {
+ this.displayError(
+ 'sending raw data',
+ { name: 'Error', message: 'No file selected' },
+ true,
+ )
+ return
+ }
+ let bufView
+ try {
+ const buffer = await this.rawCmdFile.arrayBuffer()
+ bufView = new Uint8Array(buffer)
+ } catch (err) {
+ this.displaySendRaw = false
+ this.displayError('sending raw data', err, true)
+ return
+ }
+ const jstr = { json_class: 'String', raw: Array.from(bufView) }
+ 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)
+ },
+ )
+ },
cancelRawCmd() {
this.displaySendRaw = false
diff --git a/openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/CommandEditor.vue b/openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/CommandEditor.vue
index 6d214efe5c..40d8da0217 100644
--- a/openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/CommandEditor.vue
+++ b/openc3-cosmos-init/plugins/packages/openc3-vue-common/src/components/CommandEditor.vue
@@ -80,6 +80,7 @@
{{ item.title }}
@@ -87,6 +88,14 @@
+
+
v === true)
+ },
+ contextMenuOptions() {
+ const options = [
{
title: 'Details',
+ dataTest: 'cmd-param-details',
action: () => {
this.contextMenuShown = false
this.viewDetails = true
},
},
- ],
- }
- },
- computed: {
- hasHazardousParameter() {
- return Object.values(this.hazardousParameters).some((v) => v === true)
+ ]
+ if (this.selectedRow && this.selectedRow.type === 'BLOCK') {
+ options.push({
+ title: 'Upload Data',
+ dataTest: 'cmd-param-upload-data',
+ action: () => {
+ this.contextMenuShown = false
+ this.openBlockUploadPicker()
+ },
+ })
+ }
+ return options
},
},
created() {
@@ -232,6 +256,7 @@ export default {
showContextMenu(event, row) {
event.preventDefault()
this.parameterName = row.item.parameter_name
+ this.selectedRow = row.item
this.contextMenuShown = false
this.x = event.clientX
this.y = event.clientY
@@ -239,6 +264,32 @@ export default {
this.contextMenuShown = true
})
},
+ openBlockUploadPicker() {
+ const input = this.$refs.blockUploadInput
+ if (!input) return
+ // Reset so re-selecting the same file still fires @change
+ input.value = ''
+ input.click()
+ },
+ async onBlockUploadFile(event) {
+ const file = event.target.files && event.target.files[0]
+ if (!file || !this.selectedRow) return
+ const targetRow = this.selectedRow
+ try {
+ const buffer = await file.arrayBuffer()
+ const bytes = new Uint8Array(buffer)
+ let hex = '0x'
+ for (const byte of bytes) {
+ hex += byte.toString(16).padStart(2, '0').toUpperCase()
+ }
+ // Setting val on the row updates the bound CommandParameterEditor.
+ // The "0x..." prefix lets convertToValue parse it as a binary BLOCK.
+ targetRow.val = hex
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error('Error reading uploaded file:', err)
+ }
+ },
updateCmdParams() {
this.ignoredParams = []
this.computedRows = []
diff --git a/playwright/tests/command-sender.p.spec.ts b/playwright/tests/command-sender.p.spec.ts
index ef83dafd13..dafa99f8d3 100644
--- a/playwright/tests/command-sender.p.spec.ts
+++ b/playwright/tests/command-sender.p.spec.ts
@@ -760,3 +760,62 @@ test('sends manually entered state values', async ({ page, utils }) => {
// Close the dropdown by pressing Escape
await page.keyboard.press('Escape')
})
+
+test('uploads data into a BLOCK parameter via right-click', async ({
+ page,
+ utils,
+}) => {
+ await page.locator('[data-test="clear-history"]').click()
+ // INST MEMLOAD has a single BLOCK parameter named DATA
+ await utils.selectTargetPacketItem('INST', 'MEMLOAD')
+ await expect(page.locator('main')).toContainText('Parameters')
+
+ const dataRow = page.locator('tr:has(td:text-is("DATA"))')
+
+ // Set up the file chooser before right-clicking so the picker is captured
+ const fileChooserPromise = page.waitForEvent('filechooser')
+
+ // Right-click on the DATA row to open the context menu
+ await dataRow.click({ button: 'right' })
+ // The new Upload Data entry should be present for BLOCK parameters
+ const uploadItem = page.locator('[data-test=cmd-param-upload-data]')
+ await expect(uploadItem).toBeVisible()
+ await uploadItem.click()
+
+ // Provide a small payload via the file chooser
+ const fileChooser = await fileChooserPromise
+ await fileChooser.setFiles({
+ name: 'upload.bin',
+ mimeType: 'application/octet-stream',
+ buffer: Buffer.from([0xde, 0xad, 0xbe, 0xef]),
+ })
+
+ // The BLOCK text field should now show the hex representation
+ await expect(
+ dataRow.locator('[data-test=cmd-param-value] input').first(),
+ ).toHaveValue('0xDEADBEEF')
+
+ // Non-BLOCK parameters should not see Upload Data
+ await utils.selectTargetPacketItem('INST', 'COLLECT')
+ await page.locator('tr:has(td:text-is("DURATION"))').click({ button: 'right' })
+ await expect(
+ page.locator('[data-test=cmd-param-upload-data]'),
+ ).not.toBeVisible()
+ await page.keyboard.press('Escape')
+})
+
+test('opens the Send Raw File dialog and lists interfaces', async ({
+ page,
+}) => {
+ // Open File > Send Raw File from the top bar
+ await page.locator('[data-test=command-sender-file]').click()
+ await page.locator('[data-test=command-sender-file-send-raw-file]').click()
+
+ // Dialog should be visible and show the interface picker
+ await expect(page.getByText('Send Raw')).toBeVisible()
+ await expect(page.locator('[data-test=send-raw-file]')).toBeVisible()
+
+ // Cancel and confirm status is updated
+ await page.getByRole('button', { name: 'Cancel' }).click()
+ await expect(page.locator('main')).toContainText('Raw command not sent')
+})