mirror of
https://github.com/sipeed/NanoKVM-USB.git
synced 2026-09-11 05:59:57 -05:00
fix(desktop): handle serial port device disconnection event
This commit is contained in:
@@ -9,6 +9,7 @@ export enum IpcEvents {
|
||||
OPEN_SERIAL_PORT = 'open-serial-port',
|
||||
OPEN_SERIAL_PORT_RSP = 'open-serial-port-rsp',
|
||||
CLOSE_SERIAL_PORT = 'close-serial-port',
|
||||
SERIAL_PORT_DISCONNECTED = 'serial-port-disconnected',
|
||||
SEND_KEYBOARD = 'send-keyboard',
|
||||
SEND_MOUSE = 'send-mouse',
|
||||
|
||||
|
||||
@@ -1,43 +1,86 @@
|
||||
import { SerialPort as SP } from 'serialport'
|
||||
|
||||
type Options = {
|
||||
path: string
|
||||
baudRate?: number
|
||||
onDisconnect?: () => void
|
||||
}
|
||||
|
||||
export class SerialPort {
|
||||
port: SP | null
|
||||
readonly TIMEOUT = 500 // 500ms
|
||||
readonly SERIAL_BAUD_RATE = 57600
|
||||
readonly READ_TIMEOUT = 500
|
||||
|
||||
private port: SP | null
|
||||
private onDisconnect?: () => void
|
||||
|
||||
constructor() {
|
||||
this.port = null
|
||||
}
|
||||
|
||||
async init(
|
||||
path: string,
|
||||
baudRate: number = 57600,
|
||||
onOpen: (err: Error | null) => void
|
||||
): Promise<void> {
|
||||
async init(options: Options): Promise<void> {
|
||||
try {
|
||||
if (this.port?.isOpen) {
|
||||
console.log('Closing existing serial port before opening new one')
|
||||
await this.close()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
console.log(`Opening serial port: ${path} at ${baudRate} baud`)
|
||||
const path = options.path
|
||||
const baudRate = options.baudRate || this.SERIAL_BAUD_RATE
|
||||
|
||||
this.port = new SP({ path, baudRate }, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening port: ', err.message)
|
||||
} else {
|
||||
console.log(`Serial port ${path} opened successfully at ${baudRate} baud`)
|
||||
throw err
|
||||
}
|
||||
onOpen(err)
|
||||
})
|
||||
|
||||
if (options.onDisconnect) {
|
||||
this.onDisconnect = options.onDisconnect
|
||||
}
|
||||
|
||||
this.port.on('close', () => {
|
||||
console.warn('Serial port closed event received')
|
||||
this.handleDisconnect()
|
||||
})
|
||||
|
||||
this.port.on('error', (err) => {
|
||||
console.error('Serial port error:', err)
|
||||
if (this.isDisconnectError(err)) {
|
||||
this.handleDisconnect()
|
||||
}
|
||||
})
|
||||
|
||||
this.port.on('data', () => {})
|
||||
} catch (err) {
|
||||
console.error('Error opening serial port:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
private handleDisconnect(): void {
|
||||
if (this.port) {
|
||||
this.port.removeAllListeners()
|
||||
this.port = null
|
||||
}
|
||||
|
||||
if (this.onDisconnect) {
|
||||
this.onDisconnect()
|
||||
this.onDisconnect = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private isDisconnectError(err: Error): boolean {
|
||||
const msg = err.message.toLowerCase()
|
||||
return (
|
||||
msg.includes('disconnected') ||
|
||||
msg.includes('device has been lost') ||
|
||||
msg.includes('has been closed') ||
|
||||
msg.includes('no such device')
|
||||
)
|
||||
}
|
||||
|
||||
async write(data: number[]): Promise<void> {
|
||||
if (!this.port?.isOpen) {
|
||||
// throw new Error('Serial port not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,7 +97,7 @@ export class SerialPort {
|
||||
const startTime = Date.now()
|
||||
|
||||
while (result.length < minSize) {
|
||||
if (Date.now() - startTime > this.TIMEOUT) {
|
||||
if (Date.now() - startTime > this.READ_TIMEOUT) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -77,7 +120,8 @@ export class SerialPort {
|
||||
async close(): Promise<void> {
|
||||
if (this.port?.isOpen) {
|
||||
try {
|
||||
console.log('Closing serial port...')
|
||||
this.port.removeAllListeners()
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.port!.close((err) => {
|
||||
if (err) {
|
||||
@@ -89,6 +133,8 @@ export class SerialPort {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.port = null
|
||||
} catch (error) {
|
||||
console.error('close-serial-port error', error)
|
||||
throw error
|
||||
|
||||
@@ -15,7 +15,16 @@ export function registerSerialPort(): void {
|
||||
async function getSerialPorts(): Promise<string[]> {
|
||||
try {
|
||||
const ports = await SerialPort.list()
|
||||
return ports.map((port) => port.path)
|
||||
const paths = ports.map((port) => port.path)
|
||||
|
||||
return paths.sort((a, b) => {
|
||||
const aHasUSB = a.toLowerCase().includes('usb')
|
||||
const bHasUSB = b.toLowerCase().includes('usb')
|
||||
|
||||
if (aHasUSB && !bHasUSB) return -1
|
||||
if (!aHasUSB && bHasUSB) return 1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error listing serial ports:', error)
|
||||
return []
|
||||
@@ -28,13 +37,18 @@ async function openSerialPort(
|
||||
baudRate: number = 57600
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await device.serialPort.init(path, baudRate, (err) => {
|
||||
const msg = err ? err.message : ''
|
||||
e.sender.send(IpcEvents.OPEN_SERIAL_PORT_RSP, msg)
|
||||
})
|
||||
const onDisconnect = () => {
|
||||
e.sender.send(IpcEvents.SERIAL_PORT_DISCONNECTED)
|
||||
}
|
||||
|
||||
await device.serialPort.init({ path, baudRate, onDisconnect })
|
||||
|
||||
e.sender.send(IpcEvents.OPEN_SERIAL_PORT_RSP, '')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error opening serial port:', error)
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error'
|
||||
e.sender.send(IpcEvents.OPEN_SERIAL_PORT_RSP, errorMsg)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
import { IpcEvents } from '@common/ipc-events'
|
||||
import { DeviceModal } from '@renderer/components/device-modal'
|
||||
import { Device } from '@renderer/components/device'
|
||||
import { Keyboard } from '@renderer/components/keyboard'
|
||||
import { Menu } from '@renderer/components/menu'
|
||||
import { Mouse } from '@renderer/components/mouse'
|
||||
@@ -38,7 +38,6 @@ const App = (): ReactElement => {
|
||||
const setResolution = useSetAtom(resolutionAtom)
|
||||
|
||||
const [state, setState] = useState<State>('loading')
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const resolution = getVideoResolution()
|
||||
@@ -54,10 +53,6 @@ const App = (): ReactElement => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setIsConnected(videoState === 'connected' && serialPortState === 'connected')
|
||||
}, [videoState, serialPortState])
|
||||
|
||||
async function requestMediaPermissions(resolution?: Resolution): Promise<void> {
|
||||
try {
|
||||
const platform = await window.electron.ipcRenderer.invoke(IpcEvents.GET_PLATFORM)
|
||||
@@ -116,14 +111,14 @@ const App = (): ReactElement => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isConnected ? (
|
||||
<Device />
|
||||
|
||||
{videoState === 'connected' && serialPortState === 'connected' && (
|
||||
<>
|
||||
<Menu />
|
||||
<Mouse />
|
||||
{isKeyboardEnable && <Keyboard />}
|
||||
</>
|
||||
) : (
|
||||
<DeviceModal />
|
||||
)}
|
||||
|
||||
<video
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { SerialPort } from './serial-port'
|
||||
import { Video } from './video'
|
||||
|
||||
export const DeviceModal = (): ReactElement => {
|
||||
export const Connect = (): ReactElement => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [errMsg, setErrMsg] = useState('')
|
||||
33
desktop/src/renderer/src/components/device/disconnect.tsx
Normal file
33
desktop/src/renderer/src/components/device/disconnect.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ReactElement, useEffect } from 'react'
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { IpcEvents } from '@common/ipc-events'
|
||||
import {
|
||||
serialPortAtom,
|
||||
serialPortStateAtom,
|
||||
videoDeviceIdAtom,
|
||||
videoStateAtom
|
||||
} from '@renderer/jotai/device'
|
||||
|
||||
export const Disconnect = (): ReactElement => {
|
||||
const setVideoState = useSetAtom(videoStateAtom)
|
||||
const setVideoDeviceId = useSetAtom(videoDeviceIdAtom)
|
||||
const setSerialPortState = useSetAtom(serialPortStateAtom)
|
||||
const setSerialPort = useSetAtom(serialPortAtom)
|
||||
|
||||
useEffect(() => {
|
||||
const rmListener = window.electron.ipcRenderer.on(IpcEvents.SERIAL_PORT_DISCONNECTED, () => {
|
||||
setVideoState('disconnected')
|
||||
setSerialPortState('disconnected')
|
||||
|
||||
setVideoDeviceId('')
|
||||
setSerialPort('')
|
||||
})
|
||||
|
||||
return () => {
|
||||
rmListener()
|
||||
}
|
||||
}, [setSerialPort, setSerialPortState, setVideoDeviceId, setVideoState])
|
||||
|
||||
return <></>
|
||||
}
|
||||
20
desktop/src/renderer/src/components/device/index.tsx
Normal file
20
desktop/src/renderer/src/components/device/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ReactElement, useEffect, useState } from 'react'
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { serialPortStateAtom, videoStateAtom } from '@renderer/jotai/device'
|
||||
|
||||
import { Connect } from './connect'
|
||||
import { Disconnect } from './disconnect'
|
||||
|
||||
export const Device = (): ReactElement => {
|
||||
const videoState = useAtomValue(videoStateAtom)
|
||||
const serialPortState = useAtomValue(serialPortStateAtom)
|
||||
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsConnected(videoState === 'connected' && serialPortState === 'connected')
|
||||
}, [videoState, serialPortState])
|
||||
|
||||
return <>{isConnected ? <Disconnect /> : <Connect />}</>
|
||||
}
|
||||
Reference in New Issue
Block a user