mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-19 18:07:58 +00:00
project rename
This commit is contained in:
0
tabby-serial/.gitignore
vendored
Normal file
0
tabby-serial/.gitignore
vendored
Normal file
40
tabby-serial/package.json
Normal file
40
tabby-serial/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "tabby-serial",
|
||||
"version": "1.0.140",
|
||||
"description": "Serial connection manager for Tabby",
|
||||
"keywords": [
|
||||
"tabby-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": "14.14.14",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"binstring": "^0.2.1",
|
||||
"buffer-replace": "^1.0.0",
|
||||
"cli-spinner": "^0.2.10",
|
||||
"hexer": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "^9.1.9",
|
||||
"@angular/common": "^9.1.11",
|
||||
"@angular/core": "^9.1.9",
|
||||
"@angular/forms": "^9.1.11",
|
||||
"@angular/platform-browser": "^9.1.11",
|
||||
"@angular/platform-browser-dynamic": "^9.1.11",
|
||||
"@ng-bootstrap/ng-bootstrap": "^6.2.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"tabby-core": "*",
|
||||
"tabby-settings": "*",
|
||||
"tabby-terminal": "*"
|
||||
}
|
||||
}
|
278
tabby-serial/src/api.ts
Normal file
278
tabby-serial/src/api.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
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 'tabby-terminal'
|
||||
import { SerialPort } from 'serialport'
|
||||
import { Logger } from 'tabby-core'
|
||||
import { Subject, Observable, interval } from 'rxjs'
|
||||
import { debounce } from 'rxjs/operators'
|
||||
import { ReadLine, createInterface as createReadline, clearLine } from 'readline'
|
||||
import { PassThrough, Readable, Writable } from 'stream'
|
||||
|
||||
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
|
||||
scripts?: LoginScript[]
|
||||
color?: string
|
||||
inputMode?: InputMode
|
||||
inputNewlines?: NewlineMode
|
||||
outputMode?: OutputMode
|
||||
outputNewlines?: NewlineMode
|
||||
}
|
||||
|
||||
export const BAUD_RATES = [
|
||||
110, 150, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1500000,
|
||||
]
|
||||
|
||||
export interface SerialPortInfo {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
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 {
|
||||
scripts?: LoginScript[]
|
||||
serial: SerialPort
|
||||
logger: Logger
|
||||
|
||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||
private serviceMessage = new Subject<string>()
|
||||
private inputReadline: ReadLine
|
||||
private inputPromptVisible = true
|
||||
private inputReadlineInStream: Readable & Writable
|
||||
private inputReadlineOutStream: Readable & Writable
|
||||
|
||||
constructor (public connection: SerialConnection) {
|
||||
super()
|
||||
this.scripts = connection.scripts ?? []
|
||||
|
||||
this.inputReadlineInStream = new PassThrough()
|
||||
this.inputReadlineOutStream = new PassThrough()
|
||||
this.inputReadline = createReadline({
|
||||
input: this.inputReadlineInStream,
|
||||
output: this.inputReadlineOutStream,
|
||||
terminal: true,
|
||||
prompt: this.connection.inputMode === 'readline-hex' ? 'hex> ' : '> ',
|
||||
} as any)
|
||||
this.inputReadlineOutStream.on('data', data => {
|
||||
this.emitOutput(Buffer.from(data))
|
||||
})
|
||||
this.inputReadline.on('line', line => {
|
||||
this.onInput(Buffer.from(line + '\n'))
|
||||
this.resetInputPrompt()
|
||||
})
|
||||
this.output$.pipe(debounce(() => interval(500))).subscribe(() => this.onOutputSettled())
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
this.open = true
|
||||
|
||||
this.serial.on('readable', () => {
|
||||
this.onOutput(this.serial.read())
|
||||
})
|
||||
|
||||
this.serial.on('end', () => {
|
||||
this.logger.info('Shell session ended')
|
||||
if (this.open) {
|
||||
this.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
this.executeUnconditionalScripts()
|
||||
}
|
||||
|
||||
write (data: Buffer): void {
|
||||
if (this.connection.inputMode?.startsWith('readline')) {
|
||||
this.inputReadlineInStream.write(data)
|
||||
} else {
|
||||
this.onInput(data)
|
||||
}
|
||||
}
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
this.serviceMessage.complete()
|
||||
this.inputReadline.close()
|
||||
await super.destroy()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
|
||||
resize (_, __) {
|
||||
this.inputReadlineOutStream.emit('resize')
|
||||
}
|
||||
|
||||
kill (_?: string): void {
|
||||
this.serial.close()
|
||||
}
|
||||
|
||||
emitServiceMessage (msg: string): void {
|
||||
this.serviceMessage.next(msg)
|
||||
this.logger.info(stripAnsi(msg))
|
||||
}
|
||||
|
||||
async getChildProcesses (): Promise<any[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
async gracefullyKillProcess (): Promise<void> {
|
||||
this.kill('TERM')
|
||||
}
|
||||
|
||||
supportsWorkingDirectory (): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
async getWorkingDirectory (): Promise<string|null> {
|
||||
return null
|
||||
}
|
||||
|
||||
private replaceNewlines (data: Buffer, mode?: NewlineMode): Buffer {
|
||||
if (!mode) {
|
||||
return data
|
||||
}
|
||||
data = bufferReplace(data, '\r\n', '\n')
|
||||
data = bufferReplace(data, '\r', '\n')
|
||||
const replacement = {
|
||||
strip: '',
|
||||
cr: '\r',
|
||||
lf: '\n',
|
||||
crlf: '\r\n',
|
||||
}[mode]
|
||||
return bufferReplace(data, '\n', replacement)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
private onOutputSettled () {
|
||||
if (this.connection.inputMode?.startsWith('readline') && !this.inputPromptVisible) {
|
||||
this.resetInputPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
private resetInputPrompt () {
|
||||
this.emitOutput(Buffer.from('\r\n'))
|
||||
this.inputReadline.prompt(true)
|
||||
this.inputPromptVisible = true
|
||||
}
|
||||
|
||||
private onOutput (data: Buffer) {
|
||||
const dataString = data.toString()
|
||||
|
||||
if (this.connection.inputMode?.startsWith('readline')) {
|
||||
if (this.inputPromptVisible) {
|
||||
clearLine(this.inputReadlineOutStream, 0)
|
||||
this.inputPromptVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
data = this.replaceNewlines(data, this.connection.outputNewlines)
|
||||
|
||||
if (this.connection.outputMode === 'hex') {
|
||||
this.emitOutput(Buffer.concat([
|
||||
Buffer.from('\r\n'),
|
||||
Buffer.from(hexdump(data, {
|
||||
group: 1,
|
||||
gutter: 4,
|
||||
divide: colors.gray(' | '),
|
||||
emptyHuman: colors.gray('╳'),
|
||||
}).replace(/\n/g, '\r\n')),
|
||||
Buffer.from('\r\n\n'),
|
||||
]))
|
||||
} else {
|
||||
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 (re.test(dataString)) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
36
tabby-serial/src/buttonProvider.ts
Normal file
36
tabby-serial/src/buttonProvider.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Injectable, Injector } from '@angular/core'
|
||||
import { HotkeysService, ToolbarButtonProvider, ToolbarButton } from 'tabby-core'
|
||||
import { SerialService } from './services/serial.service'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor (
|
||||
private injector: Injector,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
hotkeys.matchedHotkey.subscribe(async (hotkey: string) => {
|
||||
if (hotkey === 'serial') {
|
||||
this.activate()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
activate () {
|
||||
this.injector.get(SerialService).showConnectionSelector()
|
||||
}
|
||||
|
||||
provide (): ToolbarButton[] {
|
||||
return [{
|
||||
icon: require('./icons/serial.svg'),
|
||||
weight: 5,
|
||||
title: 'Serial connections',
|
||||
touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate',
|
||||
click: () => {
|
||||
this.activate()
|
||||
},
|
||||
}]
|
||||
}
|
||||
}
|
30
tabby-serial/src/cli.ts
Normal file
30
tabby-serial/src/cli.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { CLIHandler, CLIEvent, ConfigService } from 'tabby-core'
|
||||
import { SerialService } from './services/serial.service'
|
||||
|
||||
@Injectable()
|
||||
export class SerialCLIHandler extends CLIHandler {
|
||||
firstMatchOnly = true
|
||||
priority = 0
|
||||
|
||||
constructor (
|
||||
private serial: SerialService,
|
||||
private config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handle (event: CLIEvent): Promise<boolean> {
|
||||
const op = event.argv._[0]
|
||||
|
||||
if (op === 'connect-serial') {
|
||||
const connection = this.config.store.serial.connections.find(x => x.name === event.argv.connectionName)
|
||||
if (connection) {
|
||||
this.serial.connect(connection)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
200
tabby-serial/src/components/editConnectionModal.component.pug
Normal file
200
tabby-serial/src/components/editConnectionModal.component.pug
Normal file
@@ -0,0 +1,200 @@
|
||||
.modal-body
|
||||
ul.nav-tabs(ngbNav, #nav='ngbNav')
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) General
|
||||
ng-template(ngbNavContent)
|
||||
.form-group
|
||||
label Name
|
||||
input.form-control(
|
||||
type='text',
|
||||
autofocus,
|
||||
[(ngModel)]='connection.name',
|
||||
)
|
||||
|
||||
.row
|
||||
.col-6
|
||||
.form-group
|
||||
label Path
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='connection.port',
|
||||
[ngbTypeahead]='portsAutocomplete',
|
||||
[resultFormatter]='portsFormatter',
|
||||
)
|
||||
|
||||
.col-6
|
||||
.form-group
|
||||
label Baud Rate
|
||||
input.form-control(
|
||||
type='number',
|
||||
[(ngModel)]='connection.baudrate',
|
||||
[ngbTypeahead]='baudratesAutocomplete',
|
||||
)
|
||||
|
||||
.row
|
||||
.col-6
|
||||
.form-line
|
||||
.header
|
||||
.title Input mode
|
||||
|
||||
.d-flex(ngbDropdown)
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
ngbDropdownToggle,
|
||||
) {{getInputModeName(connection.inputMode)}}
|
||||
|
||||
div(ngbDropdownMenu)
|
||||
a.d-flex.flex-column(
|
||||
*ngFor='let mode of inputModes',
|
||||
(click)='connection.inputMode = mode.key',
|
||||
ngbDropdownItem
|
||||
)
|
||||
div {{mode.name}}
|
||||
.text-muted {{mode.description}}
|
||||
|
||||
.col-6
|
||||
.form-line
|
||||
.header
|
||||
.title Input newlines
|
||||
|
||||
select.form-control(
|
||||
[(ngModel)]='connection.inputNewlines',
|
||||
)
|
||||
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
|
||||
|
||||
.row
|
||||
.col-6
|
||||
.form-line
|
||||
.header
|
||||
.title Output mode
|
||||
|
||||
.d-flex(ngbDropdown)
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
ngbDropdownToggle,
|
||||
) {{getOutputModeName(connection.outputMode)}}
|
||||
|
||||
div(ngbDropdownMenu)
|
||||
a.d-flex.flex-column(
|
||||
*ngFor='let mode of outputModes',
|
||||
(click)='connection.outputMode = mode.key',
|
||||
ngbDropdownItem
|
||||
)
|
||||
div {{mode.name}}
|
||||
.text-muted {{mode.description}}
|
||||
|
||||
.col-6
|
||||
.form-line
|
||||
.header
|
||||
.title Output newlines
|
||||
|
||||
select.form-control(
|
||||
[(ngModel)]='connection.outputNewlines',
|
||||
)
|
||||
option([ngValue]='mode.key', *ngFor='let mode of newlineModes') {{mode.name}}
|
||||
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Advanced
|
||||
ng-template(ngbNavContent)
|
||||
.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')
|
||||
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Login scripts
|
||||
ng-template(ngbNavContent)
|
||||
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
|
||||
|
||||
div([ngbNavOutlet]='nav')
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='save()') Save
|
||||
button.btn.btn-outline-danger((click)='cancel()') Cancel
|
121
tabby-serial/src/components/editConnectionModal.component.ts
Normal file
121
tabby-serial/src/components/editConnectionModal.component.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'
|
||||
import { PlatformService } from 'tabby-core'
|
||||
import { SerialConnection, LoginScript, SerialPortInfo, BAUD_RATES } from '../api'
|
||||
import { SerialService } from '../services/serial.service'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./editConnectionModal.component.pug'),
|
||||
})
|
||||
export class EditConnectionModalComponent {
|
||||
connection: SerialConnection
|
||||
foundPorts: SerialPortInfo[]
|
||||
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' },
|
||||
{ key: 'strip', name: 'Strip' },
|
||||
{ key: 'cr', name: 'Force CR' },
|
||||
{ key: 'lf', name: 'Force LF' },
|
||||
{ key: 'crlf', name: 'Force CRLF' },
|
||||
]
|
||||
|
||||
constructor (
|
||||
private modalInstance: NgbActiveModal,
|
||||
private platform: PlatformService,
|
||||
private serial: SerialService,
|
||||
) {
|
||||
}
|
||||
|
||||
getInputModeName (key) {
|
||||
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)
|
||||
}))
|
||||
|
||||
baudratesAutocomplete = text$ => text$.pipe(
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
map((q: string) => BAUD_RATES.filter(x => !q || x.toString().startsWith(q)))
|
||||
)
|
||||
|
||||
portsFormatter = port => {
|
||||
const p = this.foundPorts.find(x => x.name === port)
|
||||
if (p?.description) {
|
||||
return `${port} (${p.description})`
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
this.connection.scripts = this.connection.scripts ?? []
|
||||
this.foundPorts = await this.serial.listPorts()
|
||||
}
|
||||
|
||||
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.platform.showMessageBox(
|
||||
{
|
||||
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: '' })
|
||||
}
|
||||
}
|
16
tabby-serial/src/components/serialSettingsTab.component.pug
Normal file
16
tabby-serial/src/components/serialSettingsTab.component.pug
Normal file
@@ -0,0 +1,16 @@
|
||||
h3 Connections
|
||||
|
||||
.list-group.list-group-flush.mt-3.mb-3
|
||||
.list-group-item.list-group-item-action.d-flex.align-items-center(
|
||||
*ngFor='let connection of 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
|
82
tabby-serial/src/components/serialSettingsTab.component.ts
Normal file
82
tabby-serial/src/components/serialSettingsTab.component.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ConfigService, PlatformService } from 'tabby-core'
|
||||
import { SerialConnection } from '../api'
|
||||
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./serialSettingsTab.component.pug'),
|
||||
})
|
||||
export class SerialSettingsTabComponent {
|
||||
connections: SerialConnection[]
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
private platform: PlatformService,
|
||||
private ngbModal: NgbModal,
|
||||
) {
|
||||
this.connections = this.config.store.serial.connections
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
createConnection () {
|
||||
const connection: SerialConnection = {
|
||||
name: '',
|
||||
port: '',
|
||||
baudrate: 115200,
|
||||
databits: 8,
|
||||
parity: 'none',
|
||||
rtscts: false,
|
||||
stopbits: 1,
|
||||
xany: false,
|
||||
xoff: false,
|
||||
xon: false,
|
||||
inputMode: null,
|
||||
outputMode: null,
|
||||
inputNewlines: null,
|
||||
outputNewlines: null,
|
||||
}
|
||||
|
||||
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.platform.showMessageBox(
|
||||
{
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
refresh () {
|
||||
this.connections = this.config.store.serial.connections
|
||||
}
|
||||
}
|
16
tabby-serial/src/components/serialTab.component.pug
Normal file
16
tabby-serial/src/components/serialTab.component.pug
Normal file
@@ -0,0 +1,16 @@
|
||||
.tab-toolbar([class.show]='!session || !session.open')
|
||||
.btn.btn-outline-secondary.reveal-button
|
||||
i.fas.fa-ellipsis-h
|
||||
.toolbar
|
||||
i.fas.fa-circle.text-success.mr-2(*ngIf='session && session.open')
|
||||
i.fas.fa-circle.text-danger.mr-2(*ngIf='!session || !session.open')
|
||||
strong {{connection.port}} ({{connection.baudrate}})
|
||||
|
||||
.mr-auto
|
||||
|
||||
button.btn.btn-secondary.mr-3((click)='changeBaudRate()', *ngIf='session && session.open')
|
||||
span Change baud rate
|
||||
|
||||
button.btn.btn-info((click)='reconnect()', *ngIf='!session || !session.open')
|
||||
i.fas.fa-reload
|
||||
span Reconnect
|
7
tabby-serial/src/components/serialTab.component.scss
Normal file
7
tabby-serial/src/components/serialTab.component.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
@import '../../../tabby-ssh/src/components/sshTab.component.scss';
|
||||
|
||||
:host {
|
||||
select {
|
||||
width: auto;
|
||||
}
|
||||
}
|
133
tabby-serial/src/components/serialTab.component.ts
Normal file
133
tabby-serial/src/components/serialTab.component.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import colors from 'ansi-colors'
|
||||
import { Spinner } from 'cli-spinner'
|
||||
import { Component, Injector } from '@angular/core'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { SelectorService } from 'tabby-core'
|
||||
import { BaseTerminalTabComponent } from 'tabby-terminal'
|
||||
import { SerialService } from '../services/serial.service'
|
||||
import { SerialConnection, SerialSession, BAUD_RATES } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'serial-tab',
|
||||
template: `${BaseTerminalTabComponent.template} ${require('./serialTab.component.pug')}`,
|
||||
styles: [require('./serialTab.component.scss'), ...BaseTerminalTabComponent.styles],
|
||||
animations: BaseTerminalTabComponent.animations,
|
||||
})
|
||||
export class SerialTabComponent extends BaseTerminalTabComponent {
|
||||
connection?: SerialConnection
|
||||
session: SerialSession|null = null
|
||||
serialPort: any
|
||||
private serialService: SerialService
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (
|
||||
injector: Injector,
|
||||
private selector: SelectorService,
|
||||
) {
|
||||
super(injector)
|
||||
this.serialService = injector.get(SerialService)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.logger = this.log.create('terminalTab')
|
||||
|
||||
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
switch (hotkey) {
|
||||
case 'home':
|
||||
this.sendInput('\x1b[H' )
|
||||
break
|
||||
case 'end':
|
||||
this.sendInput('\x1b[F' )
|
||||
break
|
||||
case 'restart-serial-session':
|
||||
this.reconnect()
|
||||
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
|
||||
}
|
||||
|
||||
const session = this.serialService.createSession(this.connection)
|
||||
this.setSession(session)
|
||||
this.write(`Connecting to `)
|
||||
|
||||
const spinner = new Spinner({
|
||||
text: 'Connecting',
|
||||
stream: {
|
||||
write: x => this.write(x),
|
||||
},
|
||||
})
|
||||
spinner.setSpinnerString(6)
|
||||
spinner.start()
|
||||
|
||||
try {
|
||||
this.serialPort = await this.serialService.connectSession(this.session!)
|
||||
spinner.stop(true)
|
||||
session.emitServiceMessage('Port opened')
|
||||
} 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)
|
||||
}
|
||||
|
||||
protected attachSessionHandlers () {
|
||||
this.attachSessionHandler(this.session!.serviceMessage$, msg => {
|
||||
this.write(`\r\n${colors.black.bgWhite(' Serial ')} ${msg}\r\n`)
|
||||
this.session?.resize(this.size.columns, this.size.rows)
|
||||
})
|
||||
this.attachSessionHandler(this.session!.destroyed$, () => {
|
||||
this.write('Press any key to reconnect\r\n')
|
||||
this.input$.pipe(first()).subscribe(() => {
|
||||
if (!this.session?.open) {
|
||||
this.reconnect()
|
||||
}
|
||||
})
|
||||
})
|
||||
super.attachSessionHandlers()
|
||||
}
|
||||
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
return {
|
||||
type: 'app:serial-tab',
|
||||
connection: this.connection,
|
||||
savedState: this.frontend?.saveState(),
|
||||
}
|
||||
}
|
||||
|
||||
async reconnect (): Promise<void> {
|
||||
this.session?.destroy()
|
||||
await this.initializeSession()
|
||||
this.session?.releaseInitialDataBuffer()
|
||||
}
|
||||
|
||||
async changeBaudRate () {
|
||||
const rate = await this.selector.show('Baud rate', BAUD_RATES.map(x => ({
|
||||
name: x.toString(), result: x,
|
||||
})))
|
||||
this.serialPort.update({ baudRate: rate })
|
||||
this.connection!.baudrate = rate
|
||||
}
|
||||
}
|
20
tabby-serial/src/config.ts
Normal file
20
tabby-serial/src/config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ConfigProvider } from 'tabby-core'
|
||||
|
||||
/** @hidden */
|
||||
export class SerialConfigProvider extends ConfigProvider {
|
||||
defaults = {
|
||||
serial: {
|
||||
connections: [],
|
||||
options: {
|
||||
},
|
||||
},
|
||||
hotkeys: {
|
||||
serial: [
|
||||
'Alt-K',
|
||||
],
|
||||
'restart-serial-session': [],
|
||||
},
|
||||
}
|
||||
|
||||
platformDefaults = { }
|
||||
}
|
21
tabby-serial/src/hotkeys.ts
Normal file
21
tabby-serial/src/hotkeys.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HotkeyDescription, HotkeyProvider } from 'tabby-core'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class SerialHotkeyProvider extends HotkeyProvider {
|
||||
hotkeys: HotkeyDescription[] = [
|
||||
{
|
||||
id: 'serial',
|
||||
name: 'Show Serial connections',
|
||||
},
|
||||
{
|
||||
id: 'restart-serial-session',
|
||||
name: 'Restart current serial session',
|
||||
},
|
||||
]
|
||||
|
||||
async provide (): Promise<HotkeyDescription[]> {
|
||||
return this.hotkeys
|
||||
}
|
||||
}
|
1
tabby-serial/src/icons/serial.svg
Normal file
1
tabby-serial/src/icons/serial.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg3749" width="32" height="16" version="1.1" viewBox="0 0 32 16"><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:#fff"><path style="fill-rule:nonzero;stroke-width:.10270619" id="path3739" 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)"/></g></g></svg>
|
After Width: | Height: | Size: 4.1 KiB |
50
tabby-serial/src/index.ts
Normal file
50
tabby-serial/src/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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 TabbyCoreModule, { ToolbarButtonProvider, ConfigProvider, TabRecoveryProvider, HotkeyProvider, CLIHandler } from 'tabby-core'
|
||||
import { SettingsTabProvider } from 'tabby-settings'
|
||||
import TabbyTerminalModule from 'tabby-terminal'
|
||||
|
||||
import { EditConnectionModalComponent } from './components/editConnectionModal.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'
|
||||
import { SerialCLIHandler } from './cli'
|
||||
|
||||
/** @hidden */
|
||||
@NgModule({
|
||||
imports: [
|
||||
NgbModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ToastrModule,
|
||||
TabbyCoreModule,
|
||||
TabbyTerminalModule,
|
||||
],
|
||||
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 },
|
||||
{ provide: CLIHandler, useClass: SerialCLIHandler, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
EditConnectionModalComponent,
|
||||
SerialSettingsTabComponent,
|
||||
SerialTabComponent,
|
||||
],
|
||||
declarations: [
|
||||
EditConnectionModalComponent,
|
||||
SerialSettingsTabComponent,
|
||||
SerialTabComponent,
|
||||
],
|
||||
})
|
||||
export default class SerialModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
22
tabby-serial/src/recoveryProvider.ts
Normal file
22
tabby-serial/src/recoveryProvider.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from 'tabby-core'
|
||||
|
||||
import { SerialTabComponent } from './components/serialTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
async applicableTo (recoveryToken: RecoveryToken): Promise<boolean> {
|
||||
return recoveryToken.type === 'app:serial-tab'
|
||||
}
|
||||
|
||||
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab> {
|
||||
return {
|
||||
type: SerialTabComponent,
|
||||
options: {
|
||||
connection: recoveryToken.connection,
|
||||
savedState: recoveryToken.savedState,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
179
tabby-serial/src/services/serial.service.ts
Normal file
179
tabby-serial/src/services/serial.service.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
import SerialPort from 'serialport'
|
||||
import { LogService, AppService, SelectorOption, ConfigService, NotificationsService, SelectorService } from 'tabby-core'
|
||||
import { SettingsTabComponent } from 'tabby-settings'
|
||||
import { SerialConnection, SerialSession, SerialPortInfo, BAUD_RATES } from '../api'
|
||||
import { SerialTabComponent } from '../components/serialTab.component'
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SerialService {
|
||||
private constructor (
|
||||
private log: LogService,
|
||||
private zone: NgZone,
|
||||
private notifications: NotificationsService,
|
||||
private app: AppService,
|
||||
private selector: SelectorService,
|
||||
private config: ConfigService,
|
||||
) { }
|
||||
|
||||
async listPorts (): Promise<SerialPortInfo[]> {
|
||||
return (await SerialPort.list()).map(x => ({
|
||||
name: x.path,
|
||||
description: x.manufacturer || x.serialNumber ? `${x.manufacturer || ''} ${x.serialNumber || ''}` : undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
createSession (connection: SerialConnection): SerialSession {
|
||||
const session = new SerialSession(connection)
|
||||
session.logger = this.log.create(`serial-${connection.port}`)
|
||||
return session
|
||||
}
|
||||
|
||||
async connectSession (session: SerialSession): Promise<SerialPort> {
|
||||
const serial = new SerialPort(session.connection.port, {
|
||||
autoOpen: false,
|
||||
baudRate: parseInt(session.connection.baudrate as any),
|
||||
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.notifications.error(error.toString())
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
serial.on('close', () => {
|
||||
session.emitServiceMessage('Port closed')
|
||||
session.destroy()
|
||||
})
|
||||
|
||||
try {
|
||||
serial.open()
|
||||
} catch (e) {
|
||||
this.notifications.error(e.message)
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
return serial
|
||||
}
|
||||
|
||||
async showConnectionSelector (): Promise<void> {
|
||||
const options: SelectorOption<void>[] = []
|
||||
const foundPorts = await this.listPorts()
|
||||
|
||||
try {
|
||||
const lastConnection = JSON.parse(window.localStorage.lastSerialConnection)
|
||||
if (lastConnection) {
|
||||
options.push({
|
||||
name: lastConnection.name,
|
||||
icon: 'history',
|
||||
callback: () => this.connect(lastConnection),
|
||||
})
|
||||
options.push({
|
||||
name: 'Clear last connection',
|
||||
icon: 'eraser',
|
||||
callback: () => {
|
||||
window.localStorage.lastSerialConnection = null
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch { }
|
||||
|
||||
for (const port of foundPorts) {
|
||||
options.push({
|
||||
name: port.name,
|
||||
description: port.description,
|
||||
icon: 'arrow-right',
|
||||
callback: () => this.connectFoundPort(port),
|
||||
})
|
||||
}
|
||||
|
||||
for (const connection of this.config.store.serial.connections) {
|
||||
options.push({
|
||||
name: connection.name,
|
||||
description: connection.port,
|
||||
callback: () => this.connect(connection),
|
||||
})
|
||||
}
|
||||
|
||||
options.push({
|
||||
name: 'Manage connections',
|
||||
icon: 'cog',
|
||||
callback: () => this.app.openNewTabRaw(SettingsTabComponent, { activeTab: 'serial' }),
|
||||
})
|
||||
|
||||
options.push({
|
||||
name: 'Quick connect',
|
||||
freeInputPattern: 'Open device: %s...',
|
||||
icon: 'arrow-right',
|
||||
callback: query => this.quickConnect(query),
|
||||
})
|
||||
|
||||
|
||||
await this.selector.show('Open a serial port', options)
|
||||
}
|
||||
|
||||
async connect (connection: SerialConnection): Promise<SerialTabComponent> {
|
||||
try {
|
||||
const tab = this.app.openNewTab(
|
||||
SerialTabComponent,
|
||||
{ connection }
|
||||
) as SerialTabComponent
|
||||
if (connection.color) {
|
||||
(this.app.getParentTab(tab) ?? tab).color = connection.color
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.app.activeTab?.emitFocused()
|
||||
})
|
||||
return tab
|
||||
} catch (error) {
|
||||
this.notifications.error(`Could not connect: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
quickConnect (query: string): Promise<SerialTabComponent> {
|
||||
let path = query
|
||||
let baudrate = 115200
|
||||
if (query.includes('@')) {
|
||||
baudrate = parseInt(path.split('@')[1])
|
||||
path = path.split('@')[0]
|
||||
}
|
||||
const connection: SerialConnection = {
|
||||
name: query,
|
||||
port: path,
|
||||
baudrate: baudrate,
|
||||
databits: 8,
|
||||
parity: 'none',
|
||||
rtscts: false,
|
||||
stopbits: 1,
|
||||
xany: false,
|
||||
xoff: false,
|
||||
xon: false,
|
||||
}
|
||||
window.localStorage.lastSerialConnection = JSON.stringify(connection)
|
||||
return this.connect(connection)
|
||||
}
|
||||
|
||||
async connectFoundPort (port: SerialPortInfo): Promise<SerialTabComponent> {
|
||||
const rate = await this.selector.show('Baud rate', BAUD_RATES.map(x => ({
|
||||
name: x.toString(), result: x,
|
||||
})))
|
||||
return this.quickConnect(`${port.name}@${rate}`)
|
||||
}
|
||||
}
|
16
tabby-serial/src/settings.ts
Normal file
16
tabby-serial/src/settings.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SettingsTabProvider } from 'tabby-settings'
|
||||
|
||||
import { SerialSettingsTabComponent } from './components/serialSettingsTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class SerialSettingsTabProvider extends SettingsTabProvider {
|
||||
id = 'serial'
|
||||
icon = 'keyboard'
|
||||
title = 'Serial'
|
||||
|
||||
getComponentType (): any {
|
||||
return SerialSettingsTabComponent
|
||||
}
|
||||
}
|
7
tabby-serial/tsconfig.json
Normal file
7
tabby-serial/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": ["node_modules", "dist", "typings"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
15
tabby-serial/tsconfig.typings.json
Normal file
15
tabby-serial/tsconfig.typings.json
Normal 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": {
|
||||
"tabby-*": ["../../tabby-*"],
|
||||
"*": ["../../app/node_modules/*"]
|
||||
}
|
||||
}
|
||||
}
|
5
tabby-serial/webpack.config.js
Normal file
5
tabby-serial/webpack.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = require('../webpack.plugin.config')
|
||||
module.exports = config({
|
||||
name: 'serial',
|
||||
dirname: __dirname,
|
||||
})
|
58
tabby-serial/yarn.lock
Normal file
58
tabby-serial/yarn.lock
Normal file
@@ -0,0 +1,58 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/node@14.14.14":
|
||||
version "14.14.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae"
|
||||
integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==
|
||||
|
||||
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"
|
||||
integrity sha1-vEJ8QK9MHwbWkz3t5XEQrLqK3lQ=
|
||||
|
||||
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==
|
Reference in New Issue
Block a user