Compare commits

...

11 Commits

Author SHA1 Message Date
4378d85fe8 style: dash动画 2025-12-24 16:46:09 +08:00
4943b7b611 feat: 为连接中状态添加圆形进度条 2025-12-24 16:46:00 +08:00
9d4bb5d7d9 style: 调整选择框样式 2025-12-24 16:26:28 +08:00
30e6b98cff feat: 添加DeviceName显示
- 根据usb-device.json映射vendorId与productId
- 为页面添加toast
2025-12-24 16:10:11 +08:00
d705058e1d chore!: 调整目录结构
- 将serial相关store移动至次级目录store/serial
2025-12-24 16:09:02 +08:00
6b4df26886 chore: 添加svelte-sonner 2025-12-24 16:08:21 +08:00
614130e303 feat: 实现参数设置与设备连接 2025-12-24 15:03:28 +08:00
03a56ec480 feat: 串口连接参数存储 2025-12-24 15:02:00 +08:00
b841b2641c feat: 实现SerialProvider
- SerialProvider用于为串口连接提供context
2025-12-24 15:01:38 +08:00
00bd397986 feat: 为Select组件实现DaisyUI样式 2025-12-23 14:39:03 +08:00
be210a3e36 fix: 修正文件命名
- 与组件命名一致:button.variants.ts -> Button.variants.ts
2025-12-23 14:23:48 +08:00
26 changed files with 45621 additions and 15 deletions

View File

@ -52,6 +52,7 @@
"vite": "^7.2.6"
},
"dependencies": {
"bits-ui": "^2.14.4"
"bits-ui": "^2.14.4",
"svelte-sonner": "^1.0.7"
}
}

23
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ importers:
bits-ui:
specifier: ^2.14.4
version: 2.14.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)
svelte-sonner:
specifier: ^1.0.7
version: 1.0.7(svelte@5.46.0)
devDependencies:
'@commitlint/cli':
specifier: ^20.2.0
@ -1743,6 +1746,11 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
runed@0.28.0:
resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==}
peerDependencies:
svelte: ^5.7.0
runed@0.35.1:
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
peerDependencies:
@ -1849,6 +1857,11 @@ packages:
svelte:
optional: true
svelte-sonner@1.0.7:
resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==}
peerDependencies:
svelte: ^5.0.0
svelte-toolbelt@0.10.6:
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
@ -3466,6 +3479,11 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.53.5
fsevents: 2.3.3
runed@0.28.0(svelte@5.46.0):
dependencies:
esm-env: 1.2.2
svelte: 5.46.0
runed@0.35.1(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0):
dependencies:
dequal: 2.0.3
@ -3574,6 +3592,11 @@ snapshots:
optionalDependencies:
svelte: 5.46.0
svelte-sonner@1.0.7(svelte@5.46.0):
dependencies:
runed: 0.28.0(svelte@5.46.0)
svelte: 5.46.0
svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0)(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.0)(yaml@2.8.2)))(svelte@5.46.0):
dependencies:
clsx: 2.1.1

44808
src/lib/assets/usb-device.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
<script lang="ts">
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '$lib/components/ui/tabs';
import SerialSetting from './components/SerialSetting.svelte';
let activeTab = 'serial';
</script>
<Tabs bind:value={activeTab} className="p-2">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="serial">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path
fill="currentColor"
d="M7 3h10v2h2v3h-3v6H8V8H5V5h2zm10 6h2v5h-2zm-6 6h2v7h-2zM5 9h2v5H5z"
/></svg
>
串口</TabsTrigger
>
<TabsTrigger value="ble">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 24 24"
><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path
fill="currentColor"
d="M14.88 16.29L13 18.17v-3.76m0-8.58l1.88 1.88L13 9.58m4.71-1.87L12 2h-1v7.58L6.41 5L5 6.41L10.59 12L5 17.58L6.41 19L11 14.41V22h1l5.71-5.71l-4.3-4.29z"
/></svg
>
蓝牙</TabsTrigger
>
</TabsList>
<TabsContent value="serial"><SerialSetting /></TabsContent>
<TabsContent value="ble">蓝牙设置</TabsContent>
</Tabs>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import {
Select,
SelectContent,
SelectTrigger,
} from '$lib/components/ui/select';
type Props = {
id: string;
label: string;
items: { label: string; value: string }[];
value: string;
placeholder: string;
};
let { id, label, items, value = $bindable(), placeholder }: Props = $props();
</script>
<label
for={id}
class="text-md leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{label}
</label>
<Select type="single" bind:value>
<SelectTrigger
{id}
class="w-full shadow outline-none focus:border-base-content/20 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
>
{items.find((i) => i.value === value)?.label ?? placeholder}
</SelectTrigger>
<SelectContent {items} />
</Select>

View File

@ -0,0 +1,133 @@
<script lang="ts">
import {
baudRateValue,
baudRateItems,
dataBitsValue,
parityValue,
stopBitsValue,
} from '$lib/stores/serial/serial.ui';
import { serialOptions } from '$lib/stores/serial/serial.options';
import SerialParamSelect from './SerialParamSelect.svelte';
import { toast } from 'svelte-sonner';
import { Button } from '$lib/components/ui/button';
import { getSerialContext } from '$lib/serial/serial.store';
const serial = getSerialContext();
const serialState = serial.state;
$inspect($serialState);
const dataBitsItems = [
{ value: '7', label: '7' },
{ value: '8', label: '8' },
];
const parityItems = [
{ value: 'none', label: 'None' },
{ value: 'even', label: 'Even' },
{ value: 'odd', label: 'Odd' },
];
const stopBitsItems = [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
];
async function connect() {
await serial.requestPort();
await serial.openPort($serialOptions);
if ($serialState.error) {
toast.error(`连接串口失败: ${$serialState.error}`);
} else {
toast.success('串口连接成功');
}
}
</script>
<div class="flex flex-col p-4">
<div class="flex flex-col gap-y-1.5 pb-4">
<h3 class="leading-none font-semibold tracking-tight">
{$serialState.portName ?? '串口设置'}
</h3>
<p class="text-neutral/50">请选择串口连接相关参数</p>
</div>
<div class="flex flex-col space-y-3 pb-4">
<SerialParamSelect
id="baud-rate"
label="波特率"
items={$baudRateItems}
bind:value={$baudRateValue}
placeholder="选择波特率"
/>
<SerialParamSelect
id="data-bits"
label="数据位"
items={dataBitsItems}
bind:value={$dataBitsValue}
placeholder="选择数据位"
/>
<SerialParamSelect
id="parity"
label="校验位"
items={parityItems}
bind:value={$parityValue}
placeholder="选择校验位"
/>
<SerialParamSelect
id="stop-bits"
label="停止位"
items={stopBitsItems}
bind:value={$stopBitsValue}
placeholder="选择停止位"
/>
<div class="flex grid-cols-1 flex-col gap-4">
{#if $serialState.status === 'idle'}
{#if $serialState.port !== null}
<Button class="w-full" color="primary" onclick={connect}
>重新选择设备</Button
>
<Button
class="w-full"
color="primary"
onclick={() => {
serial.reopenPort($serialOptions);
}}>重新连接</Button
>
{:else}
<Button class="w-full" color="primary" onclick={connect}
>选择串口设备</Button
>
{/if}
{:else if $serialState.status === 'requesting' || $serialState.status === 'connecting'}
<Button class="w-full" disabled
><svg class="h-5 w-5 animate-spin" viewBox="0 0 50 50">
<circle
class="animate-dash stroke-base-content/10 stroke-8"
cx="25"
cy="25"
r="20"
fill="none"
/>
</svg>
连接中...</Button
>
{:else if $serialState.status === 'connected'}
<Button class="w-full" color="error" onclick={serial.closePort}
>断开连接</Button
>
{:else}
<Button class="w-full" color="error" onclick={connect}
>连接失败,点击重试</Button
>
{/if}
</div>
</div>
</div>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { Button as BitsButton, type WithoutChildren } from 'bits-ui';
import type { Snippet } from 'svelte';
import { COLOR_MAP, VARIANT_MAP, SIZE_MAP } from './button.variants';
import type { Color, Variant, Size } from './button.variants';
import { COLOR_MAP, VARIANT_MAP, SIZE_MAP } from './Button.variants';
import type { Color, Variant, Size } from './Button.variants';
type Props = WithoutChildren<BitsButton.RootProps> & {
color?: Color;

View File

@ -0,0 +1,21 @@
export const COLOR_MAP = {
neutral: 'select-neutral',
primary: 'select-primary',
secondary: 'select-secondary',
accent: 'select-accent',
info: 'select-info',
success: 'select-success',
warning: 'select-warning',
error: 'select-error',
} as const;
export const SIZE_MAP = {
xs: 'select-xs',
sm: 'select-sm',
md: 'select-md',
lg: 'select-lg',
xl: 'select-xl',
} as const;
export type Color = keyof typeof COLOR_MAP;
export type Size = keyof typeof SIZE_MAP;

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Select as BitsSelect, type WithoutChildren } from 'bits-ui';
import { elasticOut, cubicOut } from 'svelte/easing';
import { cubicOut } from 'svelte/easing';
type Props = WithoutChildren<BitsSelect.ContentProps> & {
items: { value: string; label: string; disabled?: boolean }[];
@ -21,7 +21,7 @@
) {
const {
delay = 0,
duration = 1000,
duration = 200,
easing = cubicOut,
y = -6,
start = 0.95,
@ -59,11 +59,7 @@ transform: ${existingTransform} ${translate} ${scale};
{#snippet child({ wrapperProps, props, open })}
{#if open}
<div {...wrapperProps}>
<div
{...props}
in:selectTransition={{ easing: elasticOut }}
out:selectTransition={{ duration: 200 }}
>
<div {...props} transition:selectTransition>
<BitsSelect.ScrollUpButton>up</BitsSelect.ScrollUpButton>
<BitsSelect.Viewport class="p-1">
{#each items as { value, label, disabled } (value)}

View File

@ -2,16 +2,40 @@
import { Select as BitsSelect, type WithoutChildren } from 'bits-ui';
import type { Snippet } from 'svelte';
import { COLOR_MAP, SIZE_MAP } from './Select.variants';
import type { Color, Size } from './Select.variants';
type Props = WithoutChildren<BitsSelect.TriggerProps> & {
color?: Color;
size?: Size;
ghost?: boolean;
class?: string;
children?: Snippet;
};
let { children, class: className = '' }: Props = $props();
let {
color,
size,
ghost,
children,
class: className = '',
...restProps
}: Props = $props();
const classes = $derived(
[
'select',
color && COLOR_MAP[color],
size && SIZE_MAP[size],
ghost && 'select-ghost',
className,
]
.filter(Boolean)
.join(' ')
);
</script>
<BitsSelect.Trigger
class={`select outline-none focus:border-base-content/20 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${className}`}
>
<BitsSelect.Trigger class={classes.trim()} {...restProps}>
{@render children?.()}
</BitsSelect.Trigger>

2
src/lib/serial/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './serial.service';
export * from './serial.types';

View File

@ -0,0 +1,105 @@
<script lang="ts">
import { onDestroy, type Snippet } from 'svelte';
import { get } from 'svelte/store';
import { serialState, setSerialContext } from './serial.store';
import * as service from './serial.service';
import { getDeviceName } from '$lib/utils/device';
interface Props {
children?: Snippet;
}
let { children }: Props = $props();
async function requestPort() {
serialState.update((state) => ({
...state,
status: 'requesting',
error: undefined,
}));
try {
const port = await service.requestPort();
if (!port) return null;
const info = port.getInfo?.();
const name = info
? (getDeviceName(info) ??
`USB ${info.usbVendorId ?? ''}:${info.usbProductId ?? ''}`)
: 'Serial Device';
serialState.update((s) => ({
...s,
port,
portName: name,
status: 'idle',
}));
return port;
} catch (err) {
serialState.update((s) => ({
...s,
status: 'error',
error: String(err),
}));
return null;
}
}
async function openPort(options: SerialOptions) {
let port = get(serialState).port;
if (!port) return;
serialState.update((s) => ({
...s,
status: 'connecting',
error: undefined,
}));
try {
await service.openPort(port, options);
serialState.update((s) => ({ ...s, status: 'connected' }));
} catch (err) {
serialState.update((s) => ({
...s,
status: 'error',
error: String(err),
}));
}
}
async function closePort() {
let port = get(serialState).port;
if (!port) return;
serialState.update((s) => ({ ...s, status: 'disconnecting' }));
await service.closePort(port);
serialState.update((s) => ({
...s,
status: 'idle',
}));
}
async function reopenPort(options: SerialOptions) {
await closePort();
await openPort(options);
}
setSerialContext({
state: {
subscribe: serialState.subscribe,
},
requestPort,
openPort,
reopenPort,
closePort,
});
onDestroy(() => {
let port = get(serialState).port;
if (port) service.closePort(port);
});
</script>
{@render children?.()}

View File

@ -0,0 +1,108 @@
// src/lib/serial/serial.service.ts
// Web Serial API implementation
// Requires: @types/w3c-web-serial
/**
* 内部IO资源
*/
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
let writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
let isOpen = false;
/**
* 判断当前环境是否支持Web Serial API
*/
export function isWebSerialSupported(): boolean {
return typeof navigator !== 'undefined' && 'serial' in navigator;
}
/**
* 请求用户选择串口设备
*/
export async function requestPort(
filters?: SerialPortRequestOptions['filters']
): Promise<SerialPort | null> {
if (!isWebSerialSupported()) {
throw new Error('Web Serial API is not supported in this environment.');
}
const port = await navigator.serial.requestPort(
filters ? { filters } : undefined
);
return port ?? null;
}
/**
* 打开串口
*/
export async function openPort(
port: SerialPort,
options: SerialOptions
): Promise<void> {
if (isOpen) {
// 串口已经打开,直接返回
return;
}
await port.open(options);
isOpen = true;
// 初始化reader和writer
reader = port.readable?.getReader() ?? null;
writer = port.writable?.getWriter() ?? null;
}
/**
* 关闭串口
*/
export async function closePort(port: SerialPort) {
if (!isOpen) {
// 串口未打开,直接返回
return;
}
await reader?.cancel();
reader?.releaseLock();
await writer?.close();
writer?.releaseLock();
reader = null;
writer = null;
isOpen = false;
await port.close();
}
/**
* 向串口写入数据
*/
export async function write(data: Uint8Array): Promise<void> {
if (!writer) {
throw new Error('Serial port is not writable.');
}
await writer.write(data);
}
/**
* 开始读取串口数据循环
*/
export async function startReadLoop(
onData: (chunk: Uint8Array) => void,
onError?: (err: unknown) => void
): Promise<void> {
if (!reader) {
throw new Error('Serial port is not readable.');
}
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (value) onData(value);
}
} catch (err) {
onError?.(err);
}
}

View File

@ -0,0 +1,13 @@
import { writable } from 'svelte/store';
import { createContext } from 'svelte';
import type { SerialState, SerialContext } from './serial.types';
export const serialState = writable<SerialState>({
port: null,
portName: null,
status: 'idle',
error: undefined,
});
export const [getSerialContext, setSerialContext] =
createContext<SerialContext>();

View File

@ -0,0 +1,24 @@
import type { Readable } from 'svelte/store';
export type SerialStatus =
| 'idle'
| 'requesting'
| 'connecting'
| 'connected'
| 'disconnecting'
| 'error';
export type SerialState = {
port: SerialPort | null;
portName: string | null;
status: SerialStatus;
error?: string;
};
export type SerialContext = {
state: Readable<SerialState>;
requestPort: () => Promise<SerialPort | null>;
openPort: (options: SerialOptions) => Promise<void>;
reopenPort: (options: SerialOptions) => Promise<void>;
closePort: () => Promise<void>;
};

View File

@ -0,0 +1,29 @@
import { derived, get } from 'svelte/store';
import { baudRate, dataBits, stopBits, parity, flowControl } from './serial';
export function getSerialOptions(): SerialOptions {
return {
baudRate: get(baudRate),
dataBits: get(dataBits) as 7 | 8,
stopBits: get(stopBits) as 1 | 2,
parity: get(parity),
flowControl: get(flowControl),
};
}
export const serialOptions = derived(
[baudRate, dataBits, stopBits, parity, flowControl],
([
$baudRate,
$dataBits,
$stopBits,
$parity,
$flowControl,
]): SerialOptions => ({
baudRate: $baudRate,
dataBits: $dataBits as 7 | 8,
stopBits: $stopBits as 1 | 2,
parity: $parity,
flowControl: $flowControl,
})
);

View File

@ -0,0 +1,47 @@
import { derived, writable, get } from 'svelte/store';
import { useLocalStorage } from '../utils/useLocalStorage';
export type ReadType = 'hex' | 'ascii' | 'dec';
const defaultBaudRatesList = [9600, 19200, 38400, 57600, 115200];
/* ---------------- 派生状态 ---------------- */
export const baudRate = useLocalStorage<number>('baudRate', 9600);
export const baudRateList = useLocalStorage<number[]>(
'baudRateList',
defaultBaudRatesList
);
export const dataBits = writable<number>(8);
export const stopBits = writable<number>(1);
export const parity = writable<ParityType>('none');
export const flowControl = writable<FlowControlType>('none');
export const readType = useLocalStorage<ReadType>('readType', 'hex');
export const sendType = useLocalStorage<ReadType>('sendType', 'hex');
export const hasDecTypes = useLocalStorage<boolean>('hasDecTypes', false);
/* ---------------- 派生状态 ---------------- */
export const recordTypes = derived(hasDecTypes, ($hasDecTypes) =>
$hasDecTypes
? (['hex', 'ascii', 'dec'] satisfies ReadType[])
: (['hex', 'ascii'] satisfies ReadType[])
);
/* ---------------- Actions ---------------- */
export function nextReadType() {
const types = get(recordTypes);
const current = get(readType);
const index = types.indexOf(current);
readType.set(types[(index + 1) % types.length]);
}
export function nextSendType() {
const types = get(recordTypes);
const current = get(sendType);
const index = types.indexOf(current);
sendType.set(types[(index + 1) % types.length]);
}

View File

@ -0,0 +1,60 @@
import { derived } from 'svelte/store';
import { baudRate, baudRateList, dataBits, parity, stopBits } from './serial';
import { createUIBridge } from '../utils/uiBridge';
import { isOneOf } from '../utils/typeGuard';
export const baudRateItems = derived(baudRateList, ($list) =>
$list.map((rate) => ({
value: String(rate),
label: `${rate}`,
}))
);
const baudRateBridge = createUIBridge(
baudRate,
(v) => String(v),
(v) => {
const n = Number(v);
return Number.isFinite(n) ? n : undefined;
}
);
export const baudRateValue = baudRateBridge.store;
const dataBitsBridge = createUIBridge(
dataBits,
(v) => String(v),
(v) => {
const n = Number(v);
return n;
}
);
export const dataBitsValue = dataBitsBridge.store;
const parityBridge = createUIBridge(
parity,
(v) => String(v),
(v) => {
const PARITY_VALUES = ['none', 'odd', 'even'] as const;
if (!isOneOf(v, PARITY_VALUES)) {
console.warn(`Invalid parity value: ${v}`);
return undefined;
}
const n = v;
return n;
}
);
export const parityValue = parityBridge.store;
const stopBitsBridge = createUIBridge(
stopBits,
(v) => String(v),
(v) => {
const n = Number(v);
return n;
}
);
export const stopBitsValue = stopBitsBridge.store;

View File

@ -0,0 +1,6 @@
export function isOneOf<T extends readonly unknown[]>(
value: unknown,
allowed: T
): value is T[number] {
return allowed.includes(value as T[number]);
}

View File

@ -0,0 +1,36 @@
import { get, writable, type Writable } from 'svelte/store';
export function createUIBridge<T, U>(
source: Writable<T>,
toUI: (value: T) => U,
fromUI: (value: U) => T | undefined
) {
const ui = writable<U>(toUI(get(source)));
let syncing = false;
const unsubSource = source.subscribe((v) => {
if (syncing) return;
syncing = true;
ui.set(toUI(v));
syncing = false;
});
const unsubUI = ui.subscribe((v) => {
if (syncing) return;
const next = fromUI(v);
if (next === undefined) return;
syncing = true;
source.set(next);
syncing = false;
});
return {
store: ui,
destroy() {
unsubSource();
unsubUI();
},
};
}

View File

@ -0,0 +1,22 @@
import { writable, type Writable } from 'svelte/store';
export function useLocalStorage<T>(key: string, initialValue: T): Writable<T> {
let startValue = initialValue;
if (typeof localStorage !== 'undefined') {
const stored = localStorage.getItem(key);
if (stored !== null) {
startValue = JSON.parse(stored);
}
}
const store = writable<T>(startValue);
store.subscribe((value) => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(key, JSON.stringify(value));
}
});
return store;
}

32
src/lib/utils/device.ts Normal file
View File

@ -0,0 +1,32 @@
import USBJson from '$lib/assets/usb-device.json';
type UsbIds = {
[vendorId: string]: {
name: string;
devices: {
[productId: string]: {
devname: string;
};
};
};
};
void (USBJson satisfies UsbIds);
const USB_IDS: UsbIds = USBJson;
export function getDeviceName(info: SerialPortInfo): string | undefined {
if (!info) {
return undefined;
}
const { usbProductId, usbVendorId } = info;
if (!usbVendorId || !usbProductId) {
return undefined;
}
const vendor = USB_IDS[usbVendorId.toString(16).padStart(4, '0')];
if (!vendor) {
return undefined;
}
const product = vendor.devices[usbProductId.toString(16).padStart(4, '0')];
return product ? product.devname : undefined;
}

View File

@ -1,9 +1,13 @@
<script lang="ts">
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
import SerialProvider from '$lib/serial/serial.providers.svelte';
let { children } = $props();
</script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
{@render children()}
<SerialProvider>
{@render children()}
</SerialProvider>

View File

@ -1,6 +1,20 @@
<script lang="ts">
import DeviceSetting from '$lib/components/SettingPanel/DeviceSetting.svelte';
import { Toaster } from 'svelte-sonner';
</script>
<h1>Welcome to SvelteKit</h1>
<p>
Visit <a class="text-orange-400" href="https://svelte.dev/docs/kit"
>svelte.dev/docs/kit</a
> to read the documentation
</p>
<div class="flex">
<div class="flex w-60">
<DeviceSetting />
</div>
<div class="flex">123</div>
</div>
<Toaster />

View File

@ -1,5 +1,25 @@
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@theme {
--animate-dash: dash 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120;
}
}
}
@plugin "daisyui" {
logs: false;
}