Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion desktop/src/main/device/serial-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ export class SerialPort {
): 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`)
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`)
}
onOpen(err)
})
Expand Down Expand Up @@ -72,10 +77,24 @@ export class SerialPort {
async close(): Promise<void> {
if (this.port?.isOpen) {
try {
this.port.close()
console.log('Closing serial port...')
await new Promise<void>((resolve, reject) => {
this.port!.close((err) => {
if (err) {
console.error('close-serial-port error', err)
reject(err)
} else {
console.log('Serial port closed successfully')
resolve()
}
})
})
} catch (error) {
console.error('close-serial-port error', error)
throw error
}
} else {
console.log('Serial port is already closed or not initialized')
}
}
}
2 changes: 1 addition & 1 deletion desktop/src/main/events/serial-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async function getSerialPorts(): Promise<string[]> {
async function openSerialPort(
e: IpcMainInvokeEvent,
path: string,
baudRate = 57600
baudRate: number = 57600
): Promise<boolean> {
try {
await device.serialPort.init(path, baudRate, (err) => {
Expand Down
71 changes: 58 additions & 13 deletions desktop/src/renderer/src/components/device-modal/serial-port.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ReactElement, useEffect, useState } from 'react'
import { Select } from 'antd'
import { Select, Space } from 'antd'
import { useAtom } from 'jotai'
import { useTranslation } from 'react-i18next'

import { IpcEvents } from '@common/ipc-events'
import { serialPortAtom, serialPortStateAtom } from '@renderer/jotai/device'
import { serialPortAtom, serialPortStateAtom, baudRateAtom } from '@renderer/jotai/device'
import * as storage from '@renderer/libs/storage'

type Option = {
Expand All @@ -21,11 +21,27 @@ export const SerialPort = ({ setMsg }: SerialPortProps): ReactElement => {

const [serialPort, setSerialPort] = useAtom(serialPortAtom)
const [serialPortState, setSerialPortState] = useAtom(serialPortStateAtom)
const [baudRate, setBaudRate] = useAtom(baudRateAtom)

const [options, setOptions] = useState<Option[]>([])
const [isFailed, setIsFailed] = useState(false)

const baudRateOptions = [
{ value: 1200, label: '1200' },
{ value: 2400, label: '2400' },
{ value: 4800, label: '4800' },
{ value: 9600, label: '9600' },
{ value: 14400, label: '14400' },
{ value: 19200, label: '19200' },
{ value: 38400, label: '38400' },
{ value: 57600, label: '57600' },
{ value: 115200, label: '115200' }
]

useEffect(() => {
const savedBaudRate = storage.getBaudRate()
setBaudRate(savedBaudRate)

getSerialPorts(true)

const rmListener = window.electron.ipcRenderer.on(IpcEvents.OPEN_SERIAL_PORT_RSP, (_, err) => {
Expand Down Expand Up @@ -63,7 +79,7 @@ export const SerialPort = ({ setMsg }: SerialPortProps): ReactElement => {
setIsFailed(false)
setMsg('')

const success = await window.electron.ipcRenderer.invoke(IpcEvents.OPEN_SERIAL_PORT, port)
const success = await window.electron.ipcRenderer.invoke(IpcEvents.OPEN_SERIAL_PORT, port, baudRate)

if (success) {
setSerialPort(port)
Expand All @@ -73,16 +89,45 @@ export const SerialPort = ({ setMsg }: SerialPortProps): ReactElement => {
}
}

async function closeSerialPort(): Promise<void> {
await window.electron.ipcRenderer.invoke(IpcEvents.CLOSE_SERIAL_PORT)
setSerialPort('')
setSerialPortState('disconnected')
storage.setSerialPort('')
}

async function handleBaudRateChange(newBaudRate: number): Promise<void> {
setBaudRate(newBaudRate)
storage.setBaudRate(newBaudRate)

if (serialPort && serialPortState === 'connected') {
const currentPort = serialPort
await closeSerialPort()
setTimeout(() => {
selectSerialPort(currentPort)
}, 200)
}
}

return (
<Select
value={serialPort || undefined}
style={{ width: 280 }}
options={options}
loading={serialPortState === 'connecting'}
status={isFailed ? 'error' : undefined}
placeholder={t('modal.selectSerial')}
onChange={selectSerialPort}
onClick={() => getSerialPorts(false)}
/>
<Space direction="vertical" size="small" style={{ width: '100%', alignItems: 'center' }}>
<Select
value={serialPort || undefined}
style={{ width: 280 }}
options={options}
loading={serialPortState === 'connecting'}
status={isFailed ? 'error' : undefined}
placeholder={t('modal.selectSerial')}
onChange={selectSerialPort}
onClick={() => getSerialPorts(false)}
/>
<Select
value={baudRate}
style={{ width: 280 }}
options={baudRateOptions}
placeholder={t('modal.selectBaudRate')}
onChange={handleBaudRateChange}
/>
</Space>
)
}
112 changes: 91 additions & 21 deletions desktop/src/renderer/src/components/menu/serial-port/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
import { ReactElement, useEffect, useState } from 'react'
import { Popover } from 'antd'
import { Popover, Select, Space, Divider } from 'antd'
import clsx from 'clsx'
import { useAtom } from 'jotai'
import { CpuIcon, LoaderCircleIcon, RadioIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'

import { IpcEvents } from '@common/ipc-events'
import { serialPortAtom } from '@renderer/jotai/device'
import { serialPortAtom, baudRateAtom } from '@renderer/jotai/device'
import * as storage from '@renderer/libs/storage'

export const SerialPort = (): ReactElement => {
const { t } = useTranslation()
const [serialPort, setSerialPort] = useAtom(serialPortAtom)
const [baudRate, setBaudRate] = useAtom(baudRateAtom)

const [connectingPort, setConnectingPort] = useState('')
const [serialPorts, setSerialPorts] = useState<string[]>([])

const baudRateOptions = [
{ value: 1200, label: '1200' },
{ value: 2400, label: '2400' },
{ value: 4800, label: '4800' },
{ value: 9600, label: '9600' },
{ value: 14400, label: '14400' },
{ value: 19200, label: '19200' },
{ value: 38400, label: '38400' },
{ value: 57600, label: '57600' },
{ value: 115200, label: '115200' }
]

useEffect(() => {
const savedBaudRate = storage.getBaudRate()
setBaudRate(savedBaudRate)

getSerialPorts()

const rmListener = window.electron.ipcRenderer.on(IpcEvents.OPEN_SERIAL_PORT_RSP, () => {
Expand All @@ -34,37 +53,88 @@ export const SerialPort = (): ReactElement => {
if (connectingPort) return
setConnectingPort(port)

const success = await window.electron.ipcRenderer.invoke(IpcEvents.OPEN_SERIAL_PORT, port)
const success = await window.electron.ipcRenderer.invoke(IpcEvents.OPEN_SERIAL_PORT, port, baudRate)
if (success) {
setSerialPort(port)
storage.setSerialPort(port)
}
}

async function closeSerialPort(): Promise<void> {
await window.electron.ipcRenderer.invoke(IpcEvents.CLOSE_SERIAL_PORT)
setSerialPort('')
storage.setSerialPort('')
}

async function handleBaudRateChange(newBaudRate: number): Promise<void> {
setBaudRate(newBaudRate)
storage.setBaudRate(newBaudRate)

if (serialPort) {
const currentPort = serialPort
await closeSerialPort()
setTimeout(() => {
openSerialPort(currentPort)
}, 200)
}
}

const content = (
<div className="max-h-[350px] overflow-y-auto">
{serialPorts.map((port: string) => (
<div
key={port}
className={clsx(
'flex cursor-pointer items-center space-x-2 rounded px-3 py-2 hover:bg-neutral-700/60',
port === serialPort ? 'text-blue-500' : 'text-white'
)}
onClick={() => openSerialPort(port)}
>
{port === connectingPort ? (
<LoaderCircleIcon className="animate-spin" size={16} />
) : (
<RadioIcon size={16} />
)}
<span>{port}</span>
<div className="w-[280px] p-3">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>

<div>
<div className="mb-2 text-sm text-neutral-300">{t('menu.serialPort.device')}</div>
<div className="max-h-[200px] overflow-y-auto">
{serialPorts.length === 0 ? (
<div className="text-neutral-500 text-sm py-2">{t('menu.serialPort.noDeviceFound')}</div>
) : (
serialPorts.map((port: string) => (
<div
key={port}
className={clsx(
'flex cursor-pointer items-center space-x-2 rounded px-3 py-2 hover:bg-neutral-700/60',
port === serialPort ? 'text-blue-500' : 'text-white'
)}
onClick={() => openSerialPort(port)}
>
{port === connectingPort ? (
<LoaderCircleIcon className="animate-spin" size={16} />
) : (
<RadioIcon size={16} />
)}
<span>{port}</span>
</div>
))
)}
</div>
</div>

<Divider style={{ margin: 0, borderColor: '#404040' }} />

<div>
<div className="mb-2 text-sm text-neutral-300">{t('menu.serialPort.baudRate')}</div>
<Select
value={baudRate}
style={{ width: '100%' }}
options={baudRateOptions}
onChange={handleBaudRateChange}
size="small"
/>
</div>
))}
</Space>
</div>
)

return (
<Popover content={content} placement="bottomLeft" trigger="click" arrow={false}>
<div className="flex h-[28px] cursor-pointer items-center justify-center rounded px-2 text-white hover:bg-neutral-700/70">
<div
className={clsx(
"flex h-[28px] cursor-pointer items-center justify-center rounded px-2 hover:bg-neutral-700/70",
serialPort ? 'text-blue-400' : 'text-white'
)}
title={serialPort ? `${serialPort} @ ${baudRate}` : t('menu.serialPort.clickToSelect')}
>
<CpuIcon size={18} />
</div>
</Popover>
Expand Down
4 changes: 3 additions & 1 deletion desktop/src/renderer/src/components/menu/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactElement, useEffect, useState } from 'react'
import { Badge, Modal } from 'antd'
import clsx from 'clsx'
import { BadgeInfoIcon, CircleArrowUpIcon, PaletteIcon, SettingsIcon } from 'lucide-react'
import { BadgeInfoIcon, CircleArrowUpIcon, PaletteIcon, SettingsIcon, RotateCcwIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'

import { IpcEvents } from '@common/ipc-events'
Expand All @@ -10,6 +10,7 @@ import * as storage from '@renderer/libs/storage'
import { About } from './about'
import { Appearance } from './appearance'
import { Update } from './update'
import { Reset } from './reset'

export const Settings = (): ReactElement => {
const { t } = useTranslation()
Expand All @@ -32,6 +33,7 @@ export const Settings = (): ReactElement => {
const tabs = [
{ id: 'appearance', icon: <PaletteIcon size={16} />, component: <Appearance /> },
{ id: 'update', icon: <CircleArrowUpIcon size={16} />, component: <Update /> },
{ id: 'reset', icon: <RotateCcwIcon size={16} />, component: <Reset /> },
{ id: 'about', icon: <BadgeInfoIcon size={16} />, component: <About /> }
]

Expand Down
64 changes: 64 additions & 0 deletions desktop/src/renderer/src/components/menu/settings/reset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ReactElement, useState } from 'react'
import { Button, Modal } from 'antd'
import { useTranslation } from 'react-i18next'

import * as storage from '@renderer/libs/storage'

export const Reset = (): ReactElement => {
const { t } = useTranslation()
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)

function handleReset(): void {
setIsConfirmModalOpen(true)
}

function confirmReset(): void {
storage.clearAllSettings()
setIsConfirmModalOpen(false)
// 重新加载应用以应用重置
window.location.reload()
}

function cancelReset(): void {
setIsConfirmModalOpen(false)
}

return (
<>
<div className="space-y-6">
<div>
<div className="text-xl font-bold">{t('settings.reset.title')}</div>
<div className="pt-2 text-sm text-neutral-400">{t('settings.reset.description')}</div>
</div>

<div className="space-y-4">
<div className="rounded-lg border border-red-600/30 bg-red-600/10 p-4">
<div className="text-sm font-medium text-red-400">{t('settings.reset.warning')}</div>
<div className="mt-2 text-xs text-red-300">{t('settings.reset.warningDescription')}</div>
</div>

<Button
type="primary"
danger
onClick={handleReset}
className="w-full"
>
{t('settings.reset.button')}
</Button>
</div>
</div>

<Modal
title={t('settings.reset.confirmTitle')}
open={isConfirmModalOpen}
onOk={confirmReset}
onCancel={cancelReset}
okText={t('settings.reset.confirm')}
cancelText={t('settings.reset.cancel')}
okButtonProps={{ danger: true }}
>
<p>{t('settings.reset.confirmMessage')}</p>
</Modal>
</>
)
}
Loading