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
46 changes: 44 additions & 2 deletions browser/src/components/device-modal/serial-port.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { Button } from 'antd';
import { useAtom } from 'jotai';
import { useTranslation } from 'react-i18next';

import { serialStateAtom } from '@/jotai/device.ts';
import { device } from '@/libs/device';
import * as storage from '@/libs/storage';

type SerialPortProps = {
onDisconnect: () => void;
Expand All @@ -15,23 +16,64 @@ export const SerialPort = ({ setErrMsg, onDisconnect }: SerialPortProps) => {
const { t } = useTranslation();

const [serialState, setSerialState] = useAtom(serialStateAtom);
const autoConnectAttempted = useRef(false);

useEffect(() => {
const isWebSerialSupported = 'serial' in navigator;
if (!isWebSerialSupported) {
setSerialState('notSupported');
return;
}

if (!autoConnectAttempted.current) {
autoConnectAttempted.current = true;
tryAutoConnect();
}
}, [setSerialState]);

async function tryAutoConnect() {
const savedPort = storage.getSerialPort();
if (!savedPort) return;

try {
const grantedPorts = await navigator.serial.getPorts();
const match = grantedPorts.find((port) => {
const info = port.getInfo();
return (
savedPort.vendorId != null &&
savedPort.productId != null &&
info.usbVendorId === savedPort.vendorId &&
info.usbProductId === savedPort.productId
);
});

if (match) {
setSerialState('connecting');
await device.serialPort.init({ port: match, onDisconnect });
setSerialState('connected');
}
} catch (err) {
console.log('Auto-connect serial failed:', err);
}
}

const selectSerialPort = async () => {
if (serialState === 'connecting') return;
setSerialState('connecting');
setErrMsg('');

try {
const port = await navigator.serial.requestPort();
const savedPort = storage.getSerialPort();
const filters = savedPort?.vendorId != null
? [{ usbVendorId: savedPort.vendorId, usbProductId: savedPort.productId }]
: [];

const port = await navigator.serial.requestPort(filters.length ? { filters } : undefined);
await device.serialPort.init({ port, onDisconnect });

const info = port.getInfo();
storage.setSerialPort(info.usbVendorId, info.usbProductId);

setSerialState('connected');
} catch (err) {
console.log(err);
Expand Down
19 changes: 16 additions & 3 deletions browser/src/components/device-modal/video.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Select } from 'antd';
import { useAtom, useAtomValue } from 'jotai';
import { useTranslation } from 'react-i18next';
Expand All @@ -20,6 +20,7 @@ export const Video = ({ setErrMsg }: VideoProps) => {
const [videoState, setVideoState] = useAtom(videoStateAtom);

const [devices, setDevices] = useState<MediaDevice[]>([]);
const autoSelectAttempted = useRef(false);

useEffect(() => {
getDevices();
Expand Down Expand Up @@ -51,21 +52,29 @@ export const Video = ({ setErrMsg }: VideoProps) => {
});

setDevices(mediaDevices);

if (!autoSelectAttempted.current) {
autoSelectAttempted.current = true;
const savedDeviceId = storage.getVideoDevice();
if (savedDeviceId && mediaDevices.some((d) => d.videoId === savedDeviceId)) {
selectVideoDevice(savedDeviceId, mediaDevices);
}
}
} catch (err) {
console.log(err);
setErrMsg(t('camera.failed'));
}
}

async function selectVideo(videoId: string) {
async function selectVideoDevice(videoId: string, deviceList: MediaDevice[]) {
if (videoState === 'connecting') return;

if (!videoId) {
setVideoDeviceId('');
return;
}

const device = devices.find((d) => d.videoId === videoId);
const device = deviceList.find((d) => d.videoId === videoId);
if (!device) {
return;
}
Expand All @@ -90,6 +99,10 @@ export const Video = ({ setErrMsg }: VideoProps) => {
storage.setVideoDevice(videoId);
}

async function selectVideo(videoId: string) {
return selectVideoDevice(videoId, devices);
}

return (
<Select
value={videoDeviceId || undefined}
Expand Down
4 changes: 4 additions & 0 deletions browser/src/components/menu/serial-port/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react';
import { CpuIcon, Loader2Icon } from 'lucide-react';

import { device } from '@/libs/device';
import * as storage from '@/libs/storage';

export const SerialPort = () => {
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -13,6 +14,9 @@ export const SerialPort = () => {
try {
const port = await navigator.serial.requestPort();
await device.serialPort.init({ port });

const info = port.getInfo();
storage.setSerialPort(info.usbVendorId, info.usbProductId);
} finally {
setIsLoading(false);
}
Expand Down
2 changes: 1 addition & 1 deletion browser/src/jotai/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const resolutionAtom = atom<Resolution>({
height: 1080
});

export const videoScaleAtom = atom<number>(1.0);
export const videoScaleAtom = atom<number>(0);

export const videoRotationAtom = atom<Rotation>(0);

Expand Down
24 changes: 24 additions & 0 deletions browser/src/libs/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const MOUSE_MODE_KEY = 'nanokvm-usb-mouse-mode';
const MOUSE_SCROLL_DIRECTION_KEY = 'nanokvm-usb-mouse-scroll-direction';
const MOUSE_SCROLL_INTERVAL_KEY = 'nanokvm-usb-mouse-scroll-interval';
const MOUSE_JIGGLER_MODE_KEY = 'nanokvm-usb-mouse-jiggler-mode';
const SERIAL_PORT_KEY = 'nanokvm-usb-serial-port';
const KEYBOARD_SHORTCUT_KEY = 'nanokvm-usb-keyboard-shortcut';

export function getLanguage() {
Expand Down Expand Up @@ -155,3 +156,26 @@ export function getMouseJigglerMode(): 'enable' | 'disable' {
export function setMouseJigglerMode(jiggler: 'enable' | 'disable'): void {
localStorage.setItem(MOUSE_JIGGLER_MODE_KEY, jiggler);
}

export type SerialPortInfo = {
vendorId?: number;
productId?: number;
};

export function getSerialPort(): SerialPortInfo | null {
const data = localStorage.getItem(SERIAL_PORT_KEY);
if (!data) return null;
try {
return window.JSON.parse(data) as SerialPortInfo;
} catch {
return null;
}
}

export function setSerialPort(vendorId?: number, productId?: number): void {
localStorage.setItem(SERIAL_PORT_KEY, window.JSON.stringify({ vendorId, productId }));
}

export function removeSerialPort(): void {
localStorage.removeItem(SERIAL_PORT_KEY);
}