first working version of serial port integration

This commit is contained in:
Nikolaos Stefanou
2020-02-24 17:42:28 +00:00
parent 9705a1b5b5
commit 6348e7b8f0
29 changed files with 2667 additions and 3 deletions

View File

@@ -39,6 +39,7 @@
"pug-static-loader": "2.0.0",
"raw-loader": "4.0.0",
"sass-loader": "^8.0.0",
"serialport": "^8.0.0",
"shelljs": "0.8.3",
"source-code-pro": "^2.30.2",
"source-sans-pro": "3.6.0",
@@ -58,8 +59,8 @@
"*/node-abi": "^2.14.0"
},
"scripts": {
"build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
"build:typings": "tsc --project terminus-core/tsconfig.typings.json && tsc --project terminus-settings/tsconfig.typings.json && tsc --project terminus-terminal/tsconfig.typings.json && tsc --project terminus-plugin-manager/tsconfig.typings.json && tsc --project terminus-ssh/tsconfig.typings.json",
"build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js && webpack --color --config terminus-serial/webpack.config.js",
"build:typings": "tsc --project terminus-core/tsconfig.typings.json && tsc --project terminus-settings/tsconfig.typings.json && tsc --project terminus-terminal/tsconfig.typings.json && tsc --project terminus-plugin-manager/tsconfig.typings.json && tsc --project terminus-ssh/tsconfig.typings.json && tsc --project terminus-serial/tsconfig.typings.json",
"watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
"prod": "cross-env TERMINUS_DEV=1 electron app",

View File

@@ -0,0 +1,38 @@
{
"name": "terminus-serial",
"version": "1.0.99-nightly.0",
"description": "Serial connection manager for Terminus",
"keywords": [
"terminus-builtin-plugin"
],
"main": "dist/index.js",
"typings": "typings/index.d.ts",
"scripts": {
"build": "webpack --progress --color",
"watch": "webpack --progress --color --watch"
},
"files": [
"dist"
],
"author": "Eugene Pankov",
"license": "MIT",
"devDependencies": {
"@types/node": "12.7.3",
"@types/ssh2": "^0.5.35",
"ansi-colors": "^4.1.1",
"cli-spinner": "^0.2.10",
"electron-rebuild": "^1.10.0",
"serialport": "^8.0.0",
"terminus-terminal": "^1.0.98-nightly.0"
},
"peerDependencies": {
"@angular/common": "^7",
"@angular/core": "^7",
"@angular/forms": "^7",
"@ng-bootstrap/ng-bootstrap": "^1",
"rxjs": "^5",
"terminus-core": "*",
"terminus-settings": "*",
"terminus-terminal": "*"
}
}

154
terminus-serial/src/api.ts Normal file
View File

@@ -0,0 +1,154 @@
import { BaseSession } from 'terminus-terminal'
import { SerialPort } from 'serialport'
import { Logger } from 'terminus-core'
import { Subject, Observable } from 'rxjs'
export interface LoginScript {
expect: string
send: string
isRegex?: boolean
optional?: boolean
}
export interface SerialConnection {
name: string
port: string
baudrate: number
databits: number
stopbits: number
parity: string
rtscts: boolean
xon: boolean
xoff: boolean
xany: boolean
group: string | null
scripts?: LoginScript[]
color?: string
}
export class SerialSession extends BaseSession {
scripts?: LoginScript[]
serial: SerialPort
logger: Logger
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
private serviceMessage = new Subject<string>()
constructor (public connection: SerialConnection) {
super()
this.scripts = connection.scripts || []
}
async start () {
this.open = true
this.serial.on('data', data => {
const dataString = data.toString()
this.emitOutput(data)
if (this.scripts) {
let found = false
for (const script of this.scripts) {
let match = false
let cmd = ''
if (script.isRegex) {
const re = new RegExp(script.expect, 'g')
if (dataString.match(re)) {
cmd = dataString.replace(re, script.send)
match = true
found = true
}
} else {
if (dataString.includes(script.expect)) {
cmd = script.send
match = true
found = true
}
}
if (match) {
this.logger.info('Executing script: "' + cmd + '"')
this.serial.write(cmd + '\n')
this.scripts = this.scripts.filter(x => x !== script)
} else {
if (script.optional) {
this.logger.debug('Skip optional script: ' + script.expect)
found = true
this.scripts = this.scripts.filter(x => x !== script)
} else {
break
}
}
}
if (found) {
this.executeUnconditionalScripts()
}
}
})
this.serial.on('end', () => {
this.logger.info('Shell session ended')
if (this.open) {
this.destroy()
}
})
this.executeUnconditionalScripts()
}
emitServiceMessage (msg: string) {
this.serviceMessage.next(msg)
this.logger.info(msg)
}
write (data) {
if (this.serial) {
this.serial.write(data)
}
}
async destroy (): Promise<void> {
this.serviceMessage.complete()
await super.destroy()
}
resize (columns, rows) {
console.log('resize')
}
kill (signal?: string) {
console.log('valar morghulis')
}
async getChildProcesses (): Promise<any[]> {
return []
}
async gracefullyKillProcess (): Promise<void> {
this.kill('TERM')
}
async getWorkingDirectory (): Promise<string|null> {
return null
}
private executeUnconditionalScripts () {
if (this.scripts) {
for (const script of this.scripts) {
if (!script.expect) {
console.log('Executing script:', script.send)
this.serial.write(script.send + '\n')
this.scripts = this.scripts.filter(x => x !== script)
} else {
break
}
}
}
}
}
export interface SerialConnectionGroup {
name: string
connections: SerialConnection[]
}

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { HotkeysService, ToolbarButtonProvider, ToolbarButton } from 'terminus-core'
import { SerialModalComponent } from './components/serialModal.component'
/** @hidden */
@Injectable()
export class ButtonProvider extends ToolbarButtonProvider {
constructor (
private ngbModal: NgbModal,
hotkeys: HotkeysService,
) {
super()
hotkeys.matchedHotkey.subscribe(async (hotkey: string) => {
if (hotkey === 'serial') {
this.activate()
}
})
}
activate () {
this.ngbModal.open(SerialModalComponent)
}
provide (): ToolbarButton[] {
return [{
icon: require('./icons/serial.svg'),
weight: 5,
title: 'Serial connections',
touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate',
click: async () => {
this.activate()
},
}]
}
}

View File

@@ -0,0 +1,140 @@
.modal-body
ngb-tabset([activeId]='basic')
ngb-tab(id='basic')
ng-template(ngbTabTitle) General
ng-template(ngbTabContent)
.form-group
label Name
input.form-control(
type='text',
autofocus,
[(ngModel)]='connection.name',
)
.form-group
label Group
input.form-control(
type='text',
placeholder='Ungrouped',
[(ngModel)]='connection.group',
)
.form-group
label Path
input.form-control(
type='text',
[(ngModel)]='connection.port',
)
.form-group
label Baud Rate
input.form-control(
type='text',
[(ngModel)]='connection.baudrate',
)
ngb-tab(id='advanced')
ng-template(ngbTabTitle) Advanced
ng-template(ngbTabContent)
.form-line
.header
.title Tab color
input.form-control(
type='text',
autofocus,
[(ngModel)]='connection.color',
placeholder='#000000'
)
.form-line
.header
.title DataBits
input.form-control(
type='number',
placeholder='8',
[(ngModel)]='connection.databits',
)
.form-line
.header
.title StopBits
input.form-control(
type='number',
placeholder='1',
[(ngModel)]='connection.stopbits',
)
.form-line
.header
.title Parity
input.form-control(
type='text',
[(ngModel)]='connection.parity',
placeholder='none'
)
.form-line
.header
.title RTSCTS
toggle([(ngModel)]='connection.rtscts')
.form-line
.header
.title Xon
toggle([(ngModel)]='connection.xon')
.form-line
.header
.title Xoff
toggle([(ngModel)]='connection.xoff')
.form-line
.header
.title Xany
toggle([(ngModel)]='connection.xany')
ngb-tab(id='scripts')
ng-template(ngbTabTitle) Login scripts
ng-template(ngbTabContent)
table(*ngIf='connection.scripts.length > 0')
tr
th String to expect
th String to be sent
th.pl-2 Regex
th.pl-2 Optional
th.pl-2 Actions
tr(*ngFor='let script of connection.scripts')
td.pr-2
input.form-control(
type='text',
[(ngModel)]='script.expect'
)
td
input.form-control(
type='text',
[(ngModel)]='script.send'
)
td.pl-2
checkbox(
[(ngModel)]='script.isRegex',
)
td.pl-2
checkbox(
[(ngModel)]='script.optional',
)
td.pl-2
.input-group.flex-nowrap
button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)')
i.fas.fa-arrow-up
button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)')
i.fas.fa-arrow-down
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)')
i.fas.fa-trash
button.btn.btn-outline-info.mt-2((click)='addScript()')
i.fas.fa-plus
span New item
.modal-footer
button.btn.btn-outline-primary((click)='save()') Save
button.btn.btn-outline-danger((click)='cancel()') Cancel

View File

@@ -0,0 +1,77 @@
import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ElectronService, HostAppService } from 'terminus-core'
import { SerialConnection, LoginScript } from '../api'
// import { PromptModalComponent } from './promptModal.component'
/** @hidden */
@Component({
template: require('./editConnectionModal.component.pug'),
})
export class EditConnectionModalComponent {
connection: SerialConnection
constructor (
private modalInstance: NgbActiveModal,
private electron: ElectronService,
private hostApp: HostAppService,
// private ngbModal: NgbModal,
) {
}
async ngOnInit () {
this.connection.scripts = this.connection.scripts || []
}
save () {
this.modalInstance.close(this.connection)
}
cancel () {
this.modalInstance.dismiss()
}
moveScriptUp (script: LoginScript) {
if (!this.connection.scripts) {
this.connection.scripts = []
}
const index = this.connection.scripts.indexOf(script)
if (index > 0) {
this.connection.scripts.splice(index, 1)
this.connection.scripts.splice(index - 1, 0, script)
}
}
moveScriptDown (script: LoginScript) {
if (!this.connection.scripts) {
this.connection.scripts = []
}
const index = this.connection.scripts.indexOf(script)
if (index >= 0 && index < this.connection.scripts.length - 1) {
this.connection.scripts.splice(index, 1)
this.connection.scripts.splice(index + 1, 0, script)
}
}
async deleteScript (script: LoginScript) {
if (this.connection.scripts && (await this.electron.showMessageBox(
this.hostApp.getWindow(),
{
type: 'warning',
message: 'Delete this script?',
detail: script.expect,
buttons: ['Keep', 'Delete'],
defaultId: 1,
}
)).response === 1) {
this.connection.scripts = this.connection.scripts.filter(x => x !== script)
}
}
addScript () {
if (!this.connection.scripts) {
this.connection.scripts = []
}
this.connection.scripts.push({ expect: '', send: '' })
}
}

View File

@@ -0,0 +1,14 @@
.modal-body
input.form-control(
[type]='"text"',
autofocus,
[(ngModel)]='value',
#input,
[placeholder]='prompt',
(keyup.enter)='ok()',
(keyup.esc)='cancel()',
)
.d-flex.align-items-start.mt-2
button.btn.btn-primary.ml-auto(
(click)='ok()',
) Enter

View File

@@ -0,0 +1,31 @@
import { Component, Input, ViewChild, ElementRef } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
/** @hidden */
@Component({
template: require('./promptModal.component.pug'),
})
export class PromptModalComponent {
@Input() value: string
@ViewChild('input') input: ElementRef
constructor (
private modalInstance: NgbActiveModal,
) { }
ngOnInit () {
setTimeout(() => {
this.input.nativeElement.focus()
})
}
ok () {
this.modalInstance.close({
value: this.value,
})
}
cancel () {
this.modalInstance.close(null)
}
}

View File

@@ -0,0 +1,32 @@
.modal-body
input.form-control(
type='text',
[(ngModel)]='quickTarget',
autofocus,
placeholder='Quick connect: path@baudrate',
(ngModelChange)='refresh()',
(keyup.enter)='quickConnect()'
)
.list-group.mt-3(*ngIf='lastConnection')
a.list-group-item.list-group-item-action.d-flex.align-items-center((click)='connect(lastConnection)')
i.fas.fa-fw.fa-history
.mr-auto {{lastConnection.name}}
button.btn.btn-outline-danger.btn-sm((click)='clearLastConnection(); $event.stopPropagation()')
i.fas.fa-trash
.list-group.mt-3.connections-list(*ngIf='childGroups.length')
ng-container(*ngFor='let group of childGroups')
.list-group-item.list-group-item-action.d-flex.align-items-center(
(click)='groupCollapsed[group.name] = !groupCollapsed[group.name]'
)
.fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]')
.fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]')
.ml-2 {{group.name || "Ungrouped"}}
ng-container(*ngIf='!groupCollapsed[group.name]')
.list-group-item.list-group-item-action.pl-5.d-flex.align-items-center(
*ngFor='let connection of group.connections',
(click)='connect(connection)'
)
.mr-2 {{connection.name}}
.text-muted {{connection.port}}

View File

@@ -0,0 +1,5 @@
.list-group.connections-list {
display: block;
max-height: 70vh;
overflow-y: auto;
}

View File

@@ -0,0 +1,108 @@
import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { ToastrService } from 'ngx-toastr'
import { ConfigService, AppService } from 'terminus-core'
import { SettingsTabComponent } from 'terminus-settings'
import { SerialService } from '../services/serial.service'
import { SerialConnection, SerialConnectionGroup } from '../api'
/** @hidden */
@Component({
template: require('./serialModal.component.pug'),
styles: [require('./serialModal.component.scss')],
})
export class SerialModalComponent {
connections: SerialConnection[]
childFolders: SerialConnectionGroup[]
quickTarget: string
lastConnection: SerialConnection|null = null
childGroups: SerialConnectionGroup[]
groupCollapsed: {[id: string]: boolean} = {}
constructor (
public modalInstance: NgbActiveModal,
private config: ConfigService,
private serial: SerialService,
private app: AppService,
private toastr: ToastrService,
) { }
ngOnInit () {
this.connections = this.config.store.serial.connections
if (window.localStorage.lastSerialConnection) {
this.lastConnection = JSON.parse(window.localStorage.lastSerialConnection)
}
this.refresh()
}
quickConnect () {
let path = this.quickTarget
let baudrate = 115200
if (this.quickTarget.includes('@')) {
baudrate = parseInt(path.split('@')[1])
path = path.split('@')[0]
}
const connection: SerialConnection = {
name: this.quickTarget,
group: null,
port: path,
baudrate: baudrate,
databits: 8,
parity: "none",
rtscts: false,
stopbits: 1,
xany: false,
xoff: false,
xon: false,
}
window.localStorage.lastSerialConnection = JSON.stringify(connection)
this.connect(connection)
}
clearLastConnection () {
window.localStorage.lastSerialConnection = null
this.lastConnection = null
}
connect (connection: SerialConnection) {
this.close()
this.serial.openTab(connection).catch(error => {
this.toastr.error(`Could not connect: ${error}`)
}).then(() => {
setTimeout(() => {
this.app.activeTab.emitFocused()
})
})
}
manageConnections () {
this.close()
this.app.openNewTab(SettingsTabComponent, { activeTab: 'serial' })
}
close () {
this.modalInstance.close()
}
refresh () {
this.childGroups = []
let connections = this.connections
if (this.quickTarget) {
connections = connections.filter((connection: SerialConnection) => (connection.name + connection.group!).toLowerCase().includes(this.quickTarget))
}
for (const connection of connections) {
connection.group = connection.group || null
let group = this.childGroups.find(x => x.name === connection.group)
if (!group) {
group = {
name: connection.group!,
connections: [],
}
this.childGroups.push(group!)
}
group.connections.push(connection)
}
}
}

View File

@@ -0,0 +1,28 @@
h3 Connections
.list-group.list-group-flush.mt-3.mb-3
ng-container(*ngFor='let group of childGroups')
.list-group-item.list-group-item-action.d-flex.align-items-center(
(click)='groupCollapsed[group.name] = !groupCollapsed[group.name]'
)
.fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]')
.fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]')
span.ml-3.mr-auto {{group.name || "Ungrouped"}}
button.btn.btn-outline-info.ml-2((click)='editGroup(group)')
i.fas.fa-edit
button.btn.btn-outline-danger.ml-1((click)='deleteGroup(group)')
i.fas.fa-trash
ng-container(*ngIf='!groupCollapsed[group.name]')
.list-group-item.list-group-item-action.pl-5.d-flex.align-items-center(
*ngFor='let connection of group.connections',
(click)='editConnection(connection)'
)
.mr-auto
div {{connection.name}}
.text-muted {{connection.port}}
button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteConnection(connection)')
i.fas.fa-trash
button.btn.btn-primary((click)='createConnection()')
i.fas.fa-fw.fa-plus
span.ml-2 Add connection

View File

@@ -0,0 +1,131 @@
import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, ElectronService, HostAppService } from 'terminus-core'
import { SerialConnection, SerialConnectionGroup } from '../api'
import { EditConnectionModalComponent } from './editConnectionModal.component'
import { PromptModalComponent } from './promptModal.component'
/** @hidden */
@Component({
template: require('./serialSettingsTab.component.pug'),
})
export class SerialSettingsTabComponent {
connections: SerialConnection[]
childGroups: SerialConnectionGroup[]
groupCollapsed: {[id: string]: boolean} = {}
constructor (
public config: ConfigService,
private electron: ElectronService,
private hostApp: HostAppService,
private ngbModal: NgbModal,
) {
this.connections = this.config.store.serial.connections
this.refresh()
}
createConnection () {
const connection: SerialConnection = {
name: '',
group: null,
port: '',
baudrate: 115200,
databits: 8,
parity: "none",
rtscts: false,
stopbits: 1,
xany: false,
xoff: false,
xon: false,
}
const modal = this.ngbModal.open(EditConnectionModalComponent)
modal.componentInstance.connection = connection
modal.result.then(result => {
this.connections.push(result)
this.config.store.serial.connections = this.connections
this.config.save()
this.refresh()
})
}
editConnection (connection: SerialConnection) {
const modal = this.ngbModal.open(EditConnectionModalComponent, { size: 'lg' })
modal.componentInstance.connection = Object.assign({}, connection)
modal.result.then(result => {
Object.assign(connection, result)
this.config.store.serial.connections = this.connections
this.config.save()
this.refresh()
})
}
async deleteConnection (connection: SerialConnection) {
if ((await this.electron.showMessageBox(
this.hostApp.getWindow(),
{
type: 'warning',
message: `Delete "${connection.name}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 1,
}
)).response === 1) {
this.connections = this.connections.filter(x => x !== connection)
this.config.store.serial.connections = this.connections
this.config.save()
this.refresh()
}
}
editGroup (group: SerialConnectionGroup) {
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = 'New group name'
modal.componentInstance.value = group.name
modal.result.then(result => {
if (result) {
for (const connection of this.connections.filter(x => x.group === group.name)) {
connection.group = result.value
}
this.config.store.serial.connections = this.connections
this.config.save()
this.refresh()
}
})
}
async deleteGroup (group: SerialConnectionGroup) {
if ((await this.electron.showMessageBox(
this.hostApp.getWindow(),
{
type: 'warning',
message: `Delete "${group}"?`,
buttons: ['Keep', 'Delete'],
defaultId: 1,
}
)).response === 1) {
for (const connection of this.connections.filter(x => x.group === group.name)) {
connection.group = null
}
this.config.save()
this.refresh()
}
}
refresh () {
this.connections = this.config.store.serial.connections
this.childGroups = []
for (const connection of this.connections) {
connection.group = connection.group || null
let group = this.childGroups.find(x => x.name === connection.group)
if (!group) {
group = {
name: connection.group!,
connections: [],
}
this.childGroups.push(group!)
}
group.connections.push(connection)
}
}
}

View File

@@ -0,0 +1,11 @@
.serial-tab-toolbar
.btn.btn-outline-secondary.reveal-button
i.fas.fa-ellipsis-h
.toolbar(*ngIf='session', [class.show]='!session.open')
i.fas.fa-circle.text-success.mr-2(*ngIf='session.open')
i.fas.fa-circle.text-danger.mr-2(*ngIf='!session.open')
strong.mr-auto(*ngIf='session') {{session.connection.port}} ({{session.connection.baudrate}})
button.btn.btn-info((click)='reconnect()', *ngIf='!session.open')
i.fas.fa-reload
span Reconnect

View File

@@ -0,0 +1,71 @@
:host {
flex: auto;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
&> .content {
flex: auto;
position: relative;
display: block;
overflow: hidden;
margin: 15px;
}
.serial-tab-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 4;
pointer-events: none;
.reveal-button {
position: absolute;
top: 10px;
right: 30px;
border-radius: 50%;
width: 35px;
padding: 0;
height: 35px;
line-height: 35px;
transition: 0.125s opacity;
opacity: .5;
pointer-events: all;
}
&:hover .reveal-button {
opacity: 0;
}
&:hover .toolbar {
opacity: 1;
}
.toolbar {
opacity: 0;
background: rgba(0, 0, 0, .75);
padding: 10px 20px;
transition: 0.25s opacity;
display: flex;
align-items: center;
z-index: 1;
will-change: transform;
&>* {
pointer-events: all;
}
}
&.show {
.reveal-button {
opacity: 0;
}
.toolbar {
opacity: 1;
}
}
}
}

View File

@@ -0,0 +1,111 @@
import colors from 'ansi-colors'
import { Spinner } from 'cli-spinner'
import { Component } from '@angular/core'
// import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs/operators'
import { BaseTerminalTabComponent } from 'terminus-terminal'
import { SerialService } from '../services/serial.service'
import { SerialConnection, SerialSession } from '../api'
import { Subscription } from 'rxjs';
/** @hidden */
@Component({
selector: 'serial-tab',
template: BaseTerminalTabComponent.template + require<string>('./serialTab.component.pug'),
styles: [require('./serialTab.component.scss'), ...BaseTerminalTabComponent.styles],
animations: BaseTerminalTabComponent.animations,
})
export class SerialTabComponent extends BaseTerminalTabComponent {
connection: SerialConnection
serial: SerialService
session: SerialSession
// private ngbModal: NgbModal
private homeEndSubscription: Subscription
ngOnInit () {
// this.ngbModal = this.injector.get<NgbModal>(NgbModal)
this.logger = this.log.create('terminalTab')
this.serial = this.injector.get(SerialService)
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
if (!this.hasFocus) {
return
}
switch (hotkey) {
case 'home':
this.sendInput('\x1b[H' )
break
case 'end':
this.sendInput('\x1b[F' )
break
}
})
this.frontendReady$.pipe(first()).subscribe(() => {
this.initializeSession()
})
super.ngOnInit()
setImmediate(() => {
this.setTitle(this.connection.name)
})
}
async initializeSession () {
if (!this.connection) {
this.logger.error('No Serial connection info supplied')
return
}
this.session = this.serial.createSession(this.connection)
this.session.serviceMessage$.subscribe(msg => {
this.write('\r\n' + colors.black.bgWhite(' serial ') + ' ' + msg + '\r\n')
this.session.resize(this.size.columns, this.size.rows)
})
this.attachSessionHandlers()
this.write(`Connecting to `)
const spinner = new Spinner({
text: 'Connecting',
stream: {
write: x => this.write(x),
},
})
spinner.setSpinnerString(6)
spinner.start()
try {
await this.serial.connectSession(this.session, (message: string) => {
spinner.stop(true)
this.write(message + '\r\n')
spinner.start()
})
spinner.stop(true)
} catch (e) {
spinner.stop(true)
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
return
}
await this.session.start()
this.session.resize(this.size.columns, this.size.rows)
}
async getRecoveryToken (): Promise<any> {
return {
type: 'app:serial-tab',
connection: this.connection,
savedState: this.frontend?.saveState(),
}
}
reconnect () {
this.initializeSession()
}
ngOnDestroy () {
this.homeEndSubscription.unsubscribe()
super.ngOnDestroy()
}
}

View File

@@ -0,0 +1,19 @@
import { ConfigProvider } from 'terminus-core'
/** @hidden */
export class SerialConfigProvider extends ConfigProvider {
defaults = {
serial: {
connections: [],
options: {
},
},
hotkeys: {
serial: [
'Alt-K',
],
},
}
platformDefaults = { }
}

View File

@@ -0,0 +1,17 @@
import { Injectable } from '@angular/core'
import { HotkeyDescription, HotkeyProvider } from 'terminus-core'
/** @hidden */
@Injectable()
export class SerialHotkeyProvider extends HotkeyProvider {
hotkeys: HotkeyDescription[] = [
{
id: 'serial',
name: 'Show Serial connections',
},
]
async provide (): Promise<HotkeyDescription[]> {
return this.hotkeys
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="16"
viewBox="0 0 32 16"
version="1.1"
id="svg3749">
<defs
id="defs3753" />
<g
id="g3747"
transform="matrix(0.48599086,0,0,0.48599086,0.50191451,-0.299629)"
style="fill:none;fill-rule:evenodd">
<g
id="g3741"
transform="translate(-292.02353,-314.25882)"
style="fill:#ffffff">
<path
style="fill-rule:nonzero;stroke-width:0.10270619"
d="M 16.007812,0 3.2929688,0.03515625 2.2324219,0.40234375 C 0.91449728,1.1071083 0,2.575555 0,3.9863281 c 0,1.2651363 1.3074352,7.8137089 1.7402344,8.7167969 0.553077,1.153703 1.988134,2.456836 3.234375,2.9375 0.8530743,0.328933 1.4753185,0.348528 10.9804686,0.357422 9.789951,0.0091 10.106534,-0.002 11.087891,-0.369141 1.221173,-0.456851 2.835858,-1.955656 3.333984,-3.09375 0.336145,-0.767943 1.638672,-7.5615083 1.638672,-8.5488279 0,-1.4107731 -0.849384,-3.02258715 -2.234375,-3.58398435 L 28.791016,0 Z m -4.27539,4.890625 c 0.427942,0 0.812664,0.071135 1.152344,0.2128906 0.342354,0.1390812 0.631097,0.3382162 0.86914,0.5976563 0.243393,0.2674641 0.430211,0.5965523 0.558594,0.984375 0.131057,0.3878229 0.197266,0.8262256 0.197266,1.3183593 0,0.4921338 -0.06744,0.9337216 -0.201172,1.3242188 -0.131058,0.387823 -0.316645,0.711263 -0.554688,0.970703 -0.246067,0.270139 -0.536042,0.474922 -0.873047,0.611328 -0.33433,0.136407 -0.71782,0.203125 -1.148437,0.203125 -0.419919,0 -0.801456,-0.0699 -1.146484,-0.208984 C 10.243584,10.765216 9.9516549,10.563618 9.7109375,10.298828 9.4702197,10.034039 9.2834009,9.7093672 9.1523438,9.3242188 9.0239608,8.9390707 8.9609375,8.4987146 8.9609375,8.0039062 c 0,-0.4867846 0.063023,-0.9214925 0.1914063,-1.3066406 C 9.2807263,6.3094427 9.4687769,5.9766597 9.7148438,5.7011719 9.950212,5.4390572 10.242141,5.2386906 10.589844,5.0996094 10.940222,4.9605279 11.320527,4.890625 11.732422,4.890625 Z m 9.933594,0 c 0.427942,0 0.812664,0.071135 1.152343,0.2128906 0.342354,0.1390812 0.631098,0.3382162 0.869141,0.5976563 0.243392,0.2674641 0.430211,0.5965523 0.558594,0.984375 0.131057,0.3878229 0.197265,0.8262256 0.197265,1.3183593 0,0.4921338 -0.06744,0.9337216 -0.201171,1.3242188 -0.131058,0.387823 -0.316645,0.711263 -0.554688,0.970703 -0.246067,0.270139 -0.536042,0.474922 -0.873047,0.611328 -0.33433,0.136407 -0.71782,0.203125 -1.148437,0.203125 -0.419919,0 -0.801456,-0.0699 -1.146485,-0.208984 -0.342353,-0.139081 -0.634282,-0.340679 -0.875,-0.605469 C 19.403813,10.034039 19.216995,9.7093672 19.085938,9.3242188 18.957555,8.9390707 18.894531,8.4987146 18.894531,8.0039062 c 0,-0.4867846 0.06302,-0.9214925 0.191407,-1.3066406 0.128382,-0.3878229 0.316433,-0.7206059 0.5625,-0.9960937 0.235368,-0.2621147 0.527296,-0.4624813 0.875,-0.6015625 0.350377,-0.1390815 0.730683,-0.2089844 1.142578,-0.2089844 z m -16.0839848,0.125 h 2.359375 V 5.625 H 7.1582031 v 4.753906 h 0.7832031 v 0.611328 H 5.5820312 V 10.378906 H 6.3652344 V 5.625 H 5.5820312 Z m 9.9335938,0 H 17.875 V 5.625 h -0.783203 v 4.753906 H 17.875 v 0.611328 h -2.359375 v -0.611328 h 0.783203 V 5.625 h -0.783203 z m 9.933594,0 h 2.359375 V 5.625 h -0.783203 v 4.753906 h 0.783203 v 0.611328 h -2.359375 v -0.611328 h 0.783203 V 5.625 h -0.783203 z m -13.712891,0.5625 c -0.607143,0 -1.083937,0.2102191 -1.43164,0.6328125 -0.3450291,0.4199185 -0.5175786,1.0173231 -0.5175786,1.7929687 0,0.7836694 0.1762443,1.3854905 0.5292966,1.8027344 0.353053,0.4145694 0.826152,0.6210934 1.419922,0.6210934 0.59377,0 1.065638,-0.206524 1.416016,-0.6210934 0.353052,-0.4172439 0.529297,-1.019065 0.529297,-1.8027344 0,-0.7756456 -0.173782,-1.3730502 -0.521485,-1.7929687 C 12.812453,5.7883441 12.338122,5.578125 11.736328,5.578125 Z m 9.933594,0 c -0.607144,0 -1.083938,0.2102191 -1.431641,0.6328125 -0.345028,0.4199185 -0.517578,1.0173231 -0.517578,1.7929687 0,0.7836694 0.176244,1.3854905 0.529297,1.8027344 0.353052,0.4145694 0.826152,0.6210934 1.419922,0.6210934 0.59377,0 1.065638,-0.206524 1.416016,-0.6210934 0.353052,-0.4172439 0.529296,-1.019065 0.529296,-1.8027344 0,-0.7756456 -0.173781,-1.3730502 -0.521484,-1.7929687 C 22.746047,5.7883441 22.271716,5.578125 21.669922,5.578125 Z"
transform="matrix(2.0576519,0,0,2.0576519,290.99076,314.87535)"
id="path3739" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,51 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr'
import TerminusCoreModule, { ToolbarButtonProvider, ConfigProvider, TabRecoveryProvider, HotkeyProvider } from 'terminus-core'
import { SettingsTabProvider } from 'terminus-settings'
import TerminusTerminalModule from 'terminus-terminal'
import { EditConnectionModalComponent } from './components/editConnectionModal.component'
import { SerialModalComponent } from './components/serialModal.component'
import { SerialSettingsTabComponent } from './components/serialSettingsTab.component'
import { SerialTabComponent } from './components/serialTab.component'
import { ButtonProvider } from './buttonProvider'
import { SerialConfigProvider } from './config'
import { SerialSettingsTabProvider } from './settings'
import { RecoveryProvider } from './recoveryProvider'
import { SerialHotkeyProvider } from './hotkeys'
/** @hidden */
@NgModule({
imports: [
NgbModule,
CommonModule,
FormsModule,
ToastrModule,
TerminusCoreModule,
TerminusTerminalModule,
],
providers: [
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: ConfigProvider, useClass: SerialConfigProvider, multi: true },
{ provide: SettingsTabProvider, useClass: SerialSettingsTabProvider, multi: true },
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
{ provide: HotkeyProvider, useClass: SerialHotkeyProvider, multi: true },
],
entryComponents: [
EditConnectionModalComponent,
SerialModalComponent,
SerialSettingsTabComponent,
SerialTabComponent,
],
declarations: [
EditConnectionModalComponent,
SerialModalComponent,
SerialSettingsTabComponent,
SerialTabComponent,
],
})
export default class SerialModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class

View File

@@ -0,0 +1,21 @@
import { Injectable } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
import { SerialTabComponent } from './components/serialTab.component'
/** @hidden */
@Injectable()
export class RecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: any): Promise<RecoveredTab|null> {
if (recoveryToken && recoveryToken.type === 'app:serial-tab') {
return {
type: SerialTabComponent,
options: {
connection: recoveryToken.connection,
savedState: recoveryToken.savedState,
},
}
}
return null
}
}

View File

@@ -0,0 +1,68 @@
import { Injectable, NgZone } from '@angular/core'
const SerialPort = require('serialport')
import { ToastrService } from 'ngx-toastr'
import { AppService, LogService } from 'terminus-core'
import { SerialConnection, SerialSession } from '../api'
import { SerialTabComponent } from '../components/serialTab.component'
@Injectable({ providedIn: 'root' })
export class SerialService {
private constructor (
private log: LogService,
private app: AppService,
private zone: NgZone,
private toastr: ToastrService,
) {
}
async openTab (connection: SerialConnection): Promise<SerialTabComponent> {
const tab = this.zone.run(() => this.app.openNewTab(
SerialTabComponent,
{ connection }
) as SerialTabComponent)
if (connection.color) {
(this.app.getParentTab(tab) || tab).color = connection.color
}
return tab
}
createSession (connection: SerialConnection): SerialSession {
const session = new SerialSession(connection)
session.logger = this.log.create(`serial-${connection.port}`)
return session
}
async connectSession (session: SerialSession, logCallback?: (s: any) => void): Promise<void> {
const serial = new SerialPort(session.connection.port, { autoOpen: false, baudRate: session.connection.baudrate,
dataBits: session.connection.databits, stopBits: session.connection.stopbits, parity: session.connection.parity,
rtscts: session.connection.rtscts, xon: session.connection.xon, xoff: session.connection.xoff,
xany: session.connection.xany })
session.serial = serial
let connected = false
await new Promise(async (resolve, reject) => {
serial.on('open', () => {
connected = true
this.zone.run(resolve)
})
serial.on('error', error => {
this.zone.run(() => {
if (connected) {
this.toastr.error(error.toString())
} else {
reject(error)
}
})
})
try {
serial.open()
} catch (e) {
this.toastr.error(e.message)
reject(e)
}
})
}
}

View File

@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core'
import { SettingsTabProvider } from 'terminus-settings'
import { SerialSettingsTabComponent } from './components/serialSettingsTab.component'
/** @hidden */
@Injectable()
export class SerialSettingsTabProvider extends SettingsTabProvider {
id = 'serial'
icon = 'keyboard'
title = 'Serial'
getComponentType (): any {
return SerialSettingsTabComponent
}
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist", "typings"],
"compilerOptions": {
"baseUrl": "src"
}
}

View File

@@ -0,0 +1,15 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist", "typings"],
"include": ["src"],
"compilerOptions": {
"baseUrl": "src",
"emitDeclarationOnly": true,
"declaration": true,
"declarationDir": "./typings",
"paths": {
"terminus-*": ["../../terminus-*"],
"*": ["../../app/node_modules/*"]
}
}
}

View File

@@ -0,0 +1,59 @@
const path = require('path')
module.exports = {
target: 'node',
entry: 'src/index.ts',
context: __dirname,
devtool: 'source-map',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
pathinfo: true,
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-serial:///[resource-path]',
},
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},
resolve: {
modules: ['.', 'src', 'node_modules', '../app/node_modules'].map(x => path.join(__dirname, x)),
extensions: ['.ts', '.js', '.node'],
},
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'awesome-typescript-loader',
options: {
configFileName: path.resolve(__dirname, 'tsconfig.json'),
typeRoots: [
path.resolve(__dirname, 'node_modules/@types'),
path.resolve(__dirname, '../node_modules/@types'),
],
paths: {
"terminus-*": [path.resolve(__dirname, '../terminus-*')],
"*": [path.resolve(__dirname, '../app/node_modules/*')],
},
},
},
},
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
{ test: /\.svg/, use: ['svg-inline-loader'] },
],
},
externals: [
'fs',
'keytar',
'path',
'serialport',
'ngx-toastr',
'windows-process-tree/build/Release/windows_process_tree.node',
/^rxjs/,
/^@angular/,
/^@ng-bootstrap/,
/^terminus-/,
],
}

1375
terminus-serial/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ import { BaseTerminalTabComponent } from 'terminus-terminal'
import { SSHService } from '../services/ssh.service'
import { SSHConnection, SSHSession } from '../api'
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
import {Subscription} from "rxjs";
import { Subscription } from 'rxjs';
/** @hidden */
@Component({

View File

@@ -7,4 +7,5 @@ module.exports = [
require('./terminus-community-color-schemes/webpack.config.js'),
require('./terminus-plugin-manager/webpack.config.js'),
require('./terminus-ssh/webpack.config.js'),
require('./terminus-serial/webpack.config.js'),
]