diff --git a/terminus-serial/package.json b/terminus-serial/package.json index f72915cd..b29e711c 100644 --- a/terminus-serial/package.json +++ b/terminus-serial/package.json @@ -20,8 +20,10 @@ "@types/node": "14.14.14", "@types/ssh2": "^0.5.35", "ansi-colors": "^4.1.1", + "binstring": "^0.2.1", "buffer-replace": "^1.0.0", - "cli-spinner": "^0.2.10" + "cli-spinner": "^0.2.10", + "hexer": "^1.5.0" }, "peerDependencies": { "@angular/animations": "^9.1.9", diff --git a/terminus-serial/src/api.ts b/terminus-serial/src/api.ts index 8c3c852f..9e5263e4 100644 --- a/terminus-serial/src/api.ts +++ b/terminus-serial/src/api.ts @@ -1,3 +1,6 @@ +import hexdump from 'hexer' +import colors from 'ansi-colors' +import binstring from 'binstring' import stripAnsi from 'strip-ansi' import bufferReplace from 'buffer-replace' import { BaseSession } from 'terminus-terminal' @@ -30,6 +33,7 @@ export interface SerialConnection { color?: string inputMode?: InputMode inputNewlines?: NewlineMode + outputMode?: OutputMode outputNewlines?: NewlineMode } @@ -42,7 +46,8 @@ export interface SerialPortInfo { description?: string } -export type InputMode = null | 'readline' // eslint-disable-line @typescript-eslint/no-type-alias +export type InputMode = null | 'readline' | 'readline-hex' // eslint-disable-line @typescript-eslint/no-type-alias +export type OutputMode = null | 'hex' // eslint-disable-line @typescript-eslint/no-type-alias export type NewlineMode = null | 'cr' | 'lf' | 'crlf' // eslint-disable-line @typescript-eslint/no-type-alias export class SerialSession extends BaseSession { @@ -67,14 +72,14 @@ export class SerialSession extends BaseSession { input: this.inputReadlineInStream, output: this.inputReadlineOutStream, terminal: true, + prompt: this.connection.inputMode === 'readline-hex' ? 'hex> ' : '> ', } as any) this.inputReadlineOutStream.on('data', data => { - if (this.connection.inputMode === 'readline') { - this.emitOutput(data) - } + this.emitOutput(Buffer.from(data)) }) this.inputReadline.on('line', line => { this.onInput(new Buffer(line + '\n')) + this.resetInputPrompt() }) this.output$.pipe(debounce(() => interval(500))).subscribe(() => this.onOutputSettled()) } @@ -97,7 +102,7 @@ export class SerialSession extends BaseSession { } write (data: Buffer): void { - if (this.connection.inputMode === 'readline') { + if (this.connection.inputMode?.startsWith('readline')) { this.inputReadlineInStream.write(data) } else { this.onInput(data) @@ -156,6 +161,16 @@ export class SerialSession extends BaseSession { } private onInput (data: Buffer) { + if (this.connection.inputMode === 'readline-hex') { + const tokens = data.toString().split(/\s/g) + data = Buffer.concat(tokens.filter(t => !!t).map(t => { + if (t.startsWith('0x')) { + t = t.substring(2) + } + return binstring(t, { 'in': 'hex' }) + })) + } + data = this.replaceNewlines(data, this.connection.inputNewlines) if (this.serial) { this.serial.write(data.toString()) @@ -163,7 +178,7 @@ export class SerialSession extends BaseSession { } private onOutputSettled () { - if (this.connection.inputMode === 'readline' && !this.inputPromptVisible) { + if (this.connection.inputMode?.startsWith('readline') && !this.inputPromptVisible) { this.resetInputPrompt() } } @@ -177,7 +192,7 @@ export class SerialSession extends BaseSession { private onOutput (data: Buffer) { const dataString = data.toString() - if (this.connection.inputMode === 'readline') { + if (this.connection.inputMode?.startsWith('readline')) { if (this.inputPromptVisible) { clearLine(this.inputReadlineOutStream, 0) this.inputPromptVisible = false @@ -185,7 +200,21 @@ export class SerialSession extends BaseSession { } data = this.replaceNewlines(data, this.connection.outputNewlines) - this.emitOutput(data) + + if (this.connection.outputMode === 'hex') { + this.emitOutput(Buffer.concat([ + new Buffer('\r\n'), + Buffer.from(hexdump(data, { + group: 1, + gutter: 4, + divide: colors.gray(' | '), + emptyHuman: colors.gray('╳'), + }).replace(/\n/g, '\r\n')), + new Buffer('\r\n\n'), + ])) + } else { + this.emitOutput(data) + } if (this.scripts) { let found = false diff --git a/terminus-serial/src/components/editConnectionModal.component.pug b/terminus-serial/src/components/editConnectionModal.component.pug index 0231ae3a..405de4d0 100644 --- a/terminus-serial/src/components/editConnectionModal.component.pug +++ b/terminus-serial/src/components/editConnectionModal.component.pug @@ -62,19 +62,19 @@ .row .col-6 - //- .form-line + .form-line .header .title Output mode .d-flex(ngbDropdown) button.btn.btn-secondary.btn-tab-bar( ngbDropdownToggle, - ) {{getInputModeName(connection.inputMode)}} + ) {{getOutputModeName(connection.outputMode)}} div(ngbDropdownMenu) a.d-flex.flex-column( - *ngFor='let mode of inputModes', - (click)='connection.inputMode = mode.key', + *ngFor='let mode of outputModes', + (click)='connection.outputMode = mode.key', ngbDropdownItem ) div {{mode.name}} diff --git a/terminus-serial/src/components/editConnectionModal.component.ts b/terminus-serial/src/components/editConnectionModal.component.ts index 64af510a..80e7ef7d 100644 --- a/terminus-serial/src/components/editConnectionModal.component.ts +++ b/terminus-serial/src/components/editConnectionModal.component.ts @@ -18,6 +18,11 @@ export class EditConnectionModalComponent { inputModes = [ { key: null, name: 'Normal', description: 'Input is sent as you type' }, { key: 'readline', name: 'Line by line', description: 'Line editor, input is sent after you press Enter' }, + { key: 'readline-hex', name: 'Hexadecimal', description: 'Send bytes by typing in hex values' }, + ] + outputModes = [ + { key: null, name: 'Normal', description: 'Output is shown as it is received' }, + { key: 'hex', name: 'Hexadecimal', description: 'Output is shown as a hexdump' }, ] newlineModes = [ { key: null, name: 'Keep' }, @@ -39,6 +44,10 @@ export class EditConnectionModalComponent { return this.inputModes.find(x => x.key === key)?.name } + getOutputModeName (key) { + return this.outputModes.find(x => x.key === key)?.name + } + portsAutocomplete = text$ => text$.pipe(map(() => { return this.foundPorts.map(x => x.name) })) diff --git a/terminus-serial/src/components/serialSettingsTab.component.ts b/terminus-serial/src/components/serialSettingsTab.component.ts index 4def0735..0651666b 100644 --- a/terminus-serial/src/components/serialSettingsTab.component.ts +++ b/terminus-serial/src/components/serialSettingsTab.component.ts @@ -35,6 +35,7 @@ export class SerialSettingsTabComponent { xoff: false, xon: false, inputMode: null, + outputMode: null, inputNewlines: null, outputNewlines: null, } diff --git a/terminus-serial/yarn.lock b/terminus-serial/yarn.lock index 6db92f47..0b7ec483 100644 --- a/terminus-serial/yarn.lock +++ b/terminus-serial/yarn.lock @@ -27,11 +27,21 @@ "@types/node" "*" "@types/ssh2-streams" "*" +ansi-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a" + integrity sha1-PnXAN0dSF1RO12Oo21cJ+prlv5o= + ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +binstring@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/binstring/-/binstring-0.2.1.tgz#8a174d301f6d54efda550dd98bb4cb524eacd75d" + integrity sha1-ihdNMB9tVO/aVQ3Zi7TLUk6s110= + buffer-replace@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-replace/-/buffer-replace-1.0.0.tgz#bc427c40af4c1f06d6933dede57110acba8ade54" @@ -41,3 +51,28 @@ cli-spinner@^0.2.10: version "0.2.10" resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47" integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q== + +hexer@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653" + integrity sha1-uGzoCFmOip0YksVx887dhvyfBlM= + dependencies: + ansi-color "^0.2.1" + minimist "^1.1.0" + process "^0.10.0" + xtend "^4.0.0" + +minimist@^1.1.0: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +process@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/process/-/process-0.10.1.tgz#842457cc51cfed72dc775afeeafb8c6034372725" + integrity sha1-hCRXzFHP7XLcd1r+6vuMYDQ3JyU= + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==