From 116d9ecf6f24436cc2f9780d15b7b0edfa166c82 Mon Sep 17 00:00:00 2001 From: DarkAthena Date: Sun, 27 Jul 2025 00:12:52 +0800 Subject: [PATCH] Add baud rate selection and settings reset feature Introduces baud rate selection for serial port connections in the UI and persists the value in local storage. Adds a settings reset tab to the menu, allowing users to clear all application settings and reload. Updates i18n files for new UI strings and refactors serial port logic to support dynamic baud rate changes. --- desktop/src/main/device/serial-port.ts | 21 +++- desktop/src/main/events/serial-port.ts | 2 +- .../components/device-modal/serial-port.tsx | 71 +++++++++-- .../src/components/menu/serial-port/index.tsx | 112 ++++++++++++++---- .../src/components/menu/settings/index.tsx | 4 +- .../src/components/menu/settings/reset.tsx | 64 ++++++++++ desktop/src/renderer/src/i18n/locales/be.ts | 34 ++++-- desktop/src/renderer/src/i18n/locales/de.ts | 34 ++++-- desktop/src/renderer/src/i18n/locales/en.ts | 22 +++- desktop/src/renderer/src/i18n/locales/nl.ts | 34 ++++-- desktop/src/renderer/src/i18n/locales/ru.ts | 26 +++- desktop/src/renderer/src/i18n/locales/zh.ts | 22 +++- desktop/src/renderer/src/jotai/device.ts | 1 + .../src/renderer/src/libs/storage/index.ts | 28 +++++ 14 files changed, 406 insertions(+), 69 deletions(-) create mode 100644 desktop/src/renderer/src/components/menu/settings/reset.tsx diff --git a/desktop/src/main/device/serial-port.ts b/desktop/src/main/device/serial-port.ts index 248a783..142395a 100644 --- a/desktop/src/main/device/serial-port.ts +++ b/desktop/src/main/device/serial-port.ts @@ -15,12 +15,17 @@ export class SerialPort { ): Promise { 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) }) @@ -72,10 +77,24 @@ export class SerialPort { async close(): Promise { if (this.port?.isOpen) { try { - this.port.close() + console.log('Closing serial port...') + await new Promise((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') } } } diff --git a/desktop/src/main/events/serial-port.ts b/desktop/src/main/events/serial-port.ts index fbc005b..a53fe56 100644 --- a/desktop/src/main/events/serial-port.ts +++ b/desktop/src/main/events/serial-port.ts @@ -26,7 +26,7 @@ async function getSerialPorts(): Promise { async function openSerialPort( e: IpcMainInvokeEvent, path: string, - baudRate = 57600 + baudRate: number = 57600 ): Promise { try { await device.serialPort.init(path, baudRate, (err) => { diff --git a/desktop/src/renderer/src/components/device-modal/serial-port.tsx b/desktop/src/renderer/src/components/device-modal/serial-port.tsx index 7ad64e9..2f79829 100644 --- a/desktop/src/renderer/src/components/device-modal/serial-port.tsx +++ b/desktop/src/renderer/src/components/device-modal/serial-port.tsx @@ -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 = { @@ -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([]) 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) => { @@ -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) @@ -73,16 +89,45 @@ export const SerialPort = ({ setMsg }: SerialPortProps): ReactElement => { } } + async function closeSerialPort(): Promise { + await window.electron.ipcRenderer.invoke(IpcEvents.CLOSE_SERIAL_PORT) + setSerialPort('') + setSerialPortState('disconnected') + storage.setSerialPort('') + } + + async function handleBaudRateChange(newBaudRate: number): Promise { + setBaudRate(newBaudRate) + storage.setBaudRate(newBaudRate) + + if (serialPort && serialPortState === 'connected') { + const currentPort = serialPort + await closeSerialPort() + setTimeout(() => { + selectSerialPort(currentPort) + }, 200) + } + } + return ( - getSerialPorts(false)} + /> + + + ) return ( -
+
diff --git a/desktop/src/renderer/src/components/menu/settings/index.tsx b/desktop/src/renderer/src/components/menu/settings/index.tsx index c62f786..b96aefb 100644 --- a/desktop/src/renderer/src/components/menu/settings/index.tsx +++ b/desktop/src/renderer/src/components/menu/settings/index.tsx @@ -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' @@ -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() @@ -32,6 +33,7 @@ export const Settings = (): ReactElement => { const tabs = [ { id: 'appearance', icon: , component: }, { id: 'update', icon: , component: }, + { id: 'reset', icon: , component: }, { id: 'about', icon: , component: } ] diff --git a/desktop/src/renderer/src/components/menu/settings/reset.tsx b/desktop/src/renderer/src/components/menu/settings/reset.tsx new file mode 100644 index 0000000..1256977 --- /dev/null +++ b/desktop/src/renderer/src/components/menu/settings/reset.tsx @@ -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 ( + <> +
+
+
{t('settings.reset.title')}
+
{t('settings.reset.description')}
+
+ +
+
+
{t('settings.reset.warning')}
+
{t('settings.reset.warningDescription')}
+
+ + +
+
+ + +

{t('settings.reset.confirmMessage')}

+
+ + ) +} diff --git a/desktop/src/renderer/src/i18n/locales/be.ts b/desktop/src/renderer/src/i18n/locales/be.ts index ce82906..ef59a6a 100644 --- a/desktop/src/renderer/src/i18n/locales/be.ts +++ b/desktop/src/renderer/src/i18n/locales/be.ts @@ -8,15 +8,22 @@ const be = { failed: 'Kan geen verbinding maken met de camera. Probeer opnieuw.' }, modal: { - title: 'Kies USB-apparaat', - selectVideo: 'Kies een video-invoerapparaat', - selectSerial: 'Kies serieel apparaat' - }, + title: 'Kies USB-apparaat', + selectVideo: 'Selecteer een video-invoerapparaat', + selectSerial: 'Kies serieel apparaat', + selectBaudRate: 'Selecteer baudrate' + }, menu: { - serial: 'Serieel', - keyboard: 'Klavier', - mouse: 'Muis' - }, + serial: 'Serieel', + keyboard: 'Toetsenbord', + mouse: 'Muis', + serialPort: { + device: 'Serieel apparaat', + baudRate: 'Baudrate', + noDeviceFound: 'Geen seriële apparaten gevonden', + clickToSelect: 'Klik om seriële poort te selecteren' + } + }, video: { resolution: 'Resolutie', customResolution: 'Aangepast', @@ -72,6 +79,17 @@ const be = { title: 'Over', version: 'Versie', community: 'Community' + }, + reset: { + title: 'Instellingen resetten', + description: 'Alle applicatie-instellingen resetten naar standaardwaarden', + warning: 'Waarschuwing', + warningDescription: 'Deze actie kan niet ongedaan worden gemaakt. Alle aangepaste instellingen gaan verloren.', + button: 'Alle instellingen resetten', + confirmTitle: 'Reset bevestigen', + confirmMessage: 'Weet u zeker dat u alle instellingen wilt resetten? Deze actie kan niet ongedaan worden gemaakt.', + confirm: 'Reset', + cancel: 'Annuleren' } } } diff --git a/desktop/src/renderer/src/i18n/locales/de.ts b/desktop/src/renderer/src/i18n/locales/de.ts index 8a1ac47..d11a42c 100644 --- a/desktop/src/renderer/src/i18n/locales/de.ts +++ b/desktop/src/renderer/src/i18n/locales/de.ts @@ -8,15 +8,22 @@ const de = { failed: 'Kamera konnte nicht verbunden werden. Bitte versuche es erneut.' }, modal: { - title: 'USB-Gerät auswählen', - selectVideo: 'Bitte wähle ein Video-Eingabegerät aus', - selectSerial: 'Serielles Gerät auswählen' - }, + title: 'USB-Gerät auswählen', + selectVideo: 'Bitte wählen Sie ein Video-Eingabegerät', + selectSerial: 'Serielles Gerät auswählen', + selectBaudRate: 'Bitte wählen Sie die Baudrate' + }, menu: { - serial: 'Seriell', - keyboard: 'Tastatur', - mouse: 'Maus' - }, + serial: 'Seriell', + keyboard: 'Tastatur', + mouse: 'Maus', + serialPort: { + device: 'Serielles Gerät', + baudRate: 'Baudrate', + noDeviceFound: 'Keine seriellen Geräte gefunden', + clickToSelect: 'Klicken Sie, um seriellen Port auszuwählen' + } + }, video: { resolution: 'Auflösung', customResolution: 'Benutzerdefiniert', @@ -72,6 +79,17 @@ const de = { title: 'Über', version: 'Version', community: 'Community' + }, + reset: { + title: 'Einstellungen zurücksetzen', + description: 'Alle Anwendungseinstellungen auf Standardwerte zurücksetzen', + warning: 'Warnung', + warningDescription: 'Diese Aktion kann nicht rückgängig gemacht werden. Alle benutzerdefinierten Einstellungen gehen verloren.', + button: 'Alle Einstellungen zurücksetzen', + confirmTitle: 'Zurücksetzen bestätigen', + confirmMessage: 'Sind Sie sicher, dass Sie alle Einstellungen zurücksetzen möchten? Diese Aktion kann nicht rückgängig gemacht werden.', + confirm: 'Zurücksetzen', + cancel: 'Abbrechen' } } } diff --git a/desktop/src/renderer/src/i18n/locales/en.ts b/desktop/src/renderer/src/i18n/locales/en.ts index 707c660..e88c668 100644 --- a/desktop/src/renderer/src/i18n/locales/en.ts +++ b/desktop/src/renderer/src/i18n/locales/en.ts @@ -10,12 +10,19 @@ const en = { modal: { title: 'Select USB Device', selectVideo: 'Please select a video input device', - selectSerial: 'Please select serial device' + selectSerial: 'Please select serial device', + selectBaudRate: 'Please select baud rate' }, menu: { serial: 'Serial', keyboard: 'Keyboard', - mouse: 'Mouse' + mouse: 'Mouse', + serialPort: { + device: 'Serial Device', + baudRate: 'Baud Rate', + noDeviceFound: 'No serial devices found', + clickToSelect: 'Click to select serial port' + } }, video: { resolution: 'Resolution', @@ -75,6 +82,17 @@ const en = { title: 'About', version: 'Version', community: 'Community' + }, + reset: { + title: 'Reset Settings', + description: 'Reset all application settings to default values', + warning: 'Warning', + warningDescription: 'This action cannot be undone. All your custom settings will be lost.', + button: 'Reset All Settings', + confirmTitle: 'Confirm Reset', + confirmMessage: 'Are you sure you want to reset all settings? This action cannot be undone.', + confirm: 'Reset', + cancel: 'Cancel' } } } diff --git a/desktop/src/renderer/src/i18n/locales/nl.ts b/desktop/src/renderer/src/i18n/locales/nl.ts index f2dd737..dc1a86f 100644 --- a/desktop/src/renderer/src/i18n/locales/nl.ts +++ b/desktop/src/renderer/src/i18n/locales/nl.ts @@ -8,15 +8,22 @@ const nl = { failed: 'Kan geen verbinding maken met de camera. Probeer het opnieuw.' }, modal: { - title: 'Selecteer USB-apparaat', - selectVideo: 'Selecteer een video-invoerapparaat', - selectSerial: 'Selecteer serieel apparaat' - }, + title: 'Selecteer USB-apparaat', + selectVideo: 'Selecteer een video-invoerapparaat', + selectSerial: 'Selecteer serieel apparaat', + selectBaudRate: 'Selecteer baudrate' + }, menu: { - serial: 'Serieel', - keyboard: 'Toetsenbord', - mouse: 'Muis' - }, + serial: 'Serieel', + keyboard: 'Toetsenbord', + mouse: 'Muis', + serialPort: { + device: 'Serieel apparaat', + baudRate: 'Baudrate', + noDeviceFound: 'Geen seriële apparaten gevonden', + clickToSelect: 'Klik om seriële poort te selecteren' + } + }, video: { resolution: 'Resolutie', customResolution: 'Aangepast', @@ -72,6 +79,17 @@ const nl = { title: 'Over', version: 'Versie', community: 'Community' + }, + reset: { + title: 'Instellingen resetten', + description: 'Alle applicatie-instellingen resetten naar standaardwaarden', + warning: 'Waarschuwing', + warningDescription: 'Deze actie kan niet ongedaan worden gemaakt. Alle aangepaste instellingen gaan verloren.', + button: 'Alle instellingen resetten', + confirmTitle: 'Reset bevestigen', + confirmMessage: 'Weet je zeker dat je alle instellingen wilt resetten? Deze actie kan niet ongedaan worden gemaakt.', + confirm: 'Reset', + cancel: 'Annuleren' } } } diff --git a/desktop/src/renderer/src/i18n/locales/ru.ts b/desktop/src/renderer/src/i18n/locales/ru.ts index 4e50e6d..09e19c0 100644 --- a/desktop/src/renderer/src/i18n/locales/ru.ts +++ b/desktop/src/renderer/src/i18n/locales/ru.ts @@ -8,14 +8,21 @@ const ru = { failed: 'Не удалось подключить камеру. Пожалуйста, попробуйте еще раз.' }, modal: { - title: 'Выберите устройство USB', - selectVideo: 'Выбрать источник видео', - selectSerial: 'Выбрать последовательный порт' + title: 'Выбрать USB устройство', + selectVideo: 'Пожалуйста, выберите устройство видеовхода', + selectSerial: 'Выбрать последовательный порт', + selectBaudRate: 'Пожалуйста, выберите скорость передачи данных' }, menu: { serial: 'Последовательный порт', keyboard: 'Клавиатура', - mouse: 'Мышь' + mouse: 'Мышь', + serialPort: { + device: 'Последовательное устройство', + baudRate: 'Скорость передачи', + noDeviceFound: 'Последовательные устройства не найдены', + clickToSelect: 'Нажмите, чтобы выбрать последовательный порт' + } }, video: { resolution: 'Разрешение', @@ -72,6 +79,17 @@ const ru = { title: 'О системе NanoKVM-USB', version: 'Версия', community: 'Сообщество' + }, + reset: { + title: 'Сброс настроек', + description: 'Сбросить все настройки приложения к значениям по умолчанию', + warning: 'Предупреждение', + warningDescription: 'Это действие нельзя отменить. Все пользовательские настройки будут потеряны.', + button: 'Сбросить все настройки', + confirmTitle: 'Подтвердить сброс', + confirmMessage: 'Вы уверены, что хотите сбросить все настройки? Это действие нельзя отменить.', + confirm: 'Сбросить', + cancel: 'Отмена' } } } diff --git a/desktop/src/renderer/src/i18n/locales/zh.ts b/desktop/src/renderer/src/i18n/locales/zh.ts index f6ea3f1..ee37c52 100644 --- a/desktop/src/renderer/src/i18n/locales/zh.ts +++ b/desktop/src/renderer/src/i18n/locales/zh.ts @@ -9,12 +9,19 @@ const zh = { modal: { title: '选择 USB 设备', selectVideo: '请选择视频输入设备', - selectSerial: '请选择串口设备' + selectSerial: '请选择串口设备', + selectBaudRate: '请选择波特率' }, menu: { serial: '串口', keyboard: '键盘', - mouse: '鼠标' + mouse: '鼠标', + serialPort: { + device: '串口设备', + baudRate: '波特率', + noDeviceFound: '未找到串口设备', + clickToSelect: '点击选择串口' + } }, video: { resolution: '分辨率', @@ -73,6 +80,17 @@ const zh = { title: '关于', version: '版本', community: '社区' + }, + reset: { + title: '重置设置', + description: '将所有应用程序设置重置为默认值', + warning: '警告', + warningDescription: '此操作无法撤销。所有自定义设置将丢失。', + button: '重置所有设置', + confirmTitle: '确认重置', + confirmMessage: '您确定要重置所有设置吗?此操作无法撤销。', + confirm: '重置', + cancel: '取消' } } } diff --git a/desktop/src/renderer/src/jotai/device.ts b/desktop/src/renderer/src/jotai/device.ts index c42460f..448101f 100644 --- a/desktop/src/renderer/src/jotai/device.ts +++ b/desktop/src/renderer/src/jotai/device.ts @@ -15,3 +15,4 @@ export const videoStateAtom = atom('disconnected') export const serialPortAtom = atom('') export const serialPortStateAtom = atom('disconnected') +export const baudRateAtom = atom(57600) diff --git a/desktop/src/renderer/src/libs/storage/index.ts b/desktop/src/renderer/src/libs/storage/index.ts index 0024a9d..077f5c4 100644 --- a/desktop/src/renderer/src/libs/storage/index.ts +++ b/desktop/src/renderer/src/libs/storage/index.ts @@ -13,6 +13,7 @@ const MOUSE_MODE_KEY = 'nanokvm-usb-mouse-mode' const MOUSE_SCROLL_DIRECTION_KEY = 'nanokvm-usb-mouse-scroll-direction' const SKIP_UPDATE_KEY = 'nano-kvm-check-update' const MOUSE_SCROLL_INTERVAL_KEY = 'nanokvm-usb-mouse-scroll-interval' +const BAUD_RATE_KEY = 'nanokvm-usb-baud-rate' export function getLanguage(): string | null { return localStorage.getItem(LANGUAGE_KEY) @@ -71,6 +72,18 @@ export function setSerialPort(port: string): void { localStorage.setItem(SERIAL_PORT_KEY, port) } +export function getBaudRate(): number { + const baudRate = localStorage.getItem(BAUD_RATE_KEY) + if (!baudRate) { + return 57600 + } + return parseInt(baudRate, 10) +} + +export function setBaudRate(baudRate: number): void { + localStorage.setItem(BAUD_RATE_KEY, baudRate.toString()) +} + export function getIsMenuOpen(): boolean { const state = localStorage.getItem(IS_MENU_OPEN_KEY) if (!state) { @@ -132,3 +145,18 @@ export function setSkipUpdate(skip: boolean): void { const expiry = 3 * 24 * 60 * 60 * 1000 setWithExpiry(SKIP_UPDATE_KEY, String(skip), expiry) } + +export function clearAllSettings(): void { + localStorage.removeItem(LANGUAGE_KEY) + localStorage.removeItem(VIDEO_DEVICE_ID_KEY) + localStorage.removeItem(VIDEO_RESOLUTION_KEY) + localStorage.removeItem(CUSTOM_RESOLUTION_KEY) + localStorage.removeItem(SERIAL_PORT_KEY) + localStorage.removeItem(IS_MENU_OPEN_KEY) + localStorage.removeItem(MOUSE_STYLE_KEY) + localStorage.removeItem(MOUSE_MODE_KEY) + localStorage.removeItem(MOUSE_SCROLL_DIRECTION_KEY) + localStorage.removeItem(SKIP_UPDATE_KEY) + localStorage.removeItem(MOUSE_SCROLL_INTERVAL_KEY) + localStorage.removeItem(BAUD_RATE_KEY) +}