feat: 实现SerialProvider

- SerialProvider用于为串口连接提供context
This commit is contained in:
2025-12-24 15:01:38 +08:00
parent 00bd397986
commit b841b2641c
6 changed files with 255 additions and 1 deletions

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,103 @@
<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';
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
? `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

@ -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>