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') +})