feat: 实现SerialProvider
- SerialProvider用于为串口连接提供context
This commit is contained in:
2
src/lib/serial/index.ts
Normal file
2
src/lib/serial/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './serial.service';
|
||||||
|
export * from './serial.types';
|
||||||
103
src/lib/serial/serial.providers.svelte
Normal file
103
src/lib/serial/serial.providers.svelte
Normal 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?.()}
|
||||||
108
src/lib/serial/serial.service.ts
Normal file
108
src/lib/serial/serial.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/lib/serial/serial.store.ts
Normal file
13
src/lib/serial/serial.store.ts
Normal 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>();
|
||||||
24
src/lib/serial/serial.types.ts
Normal file
24
src/lib/serial/serial.types.ts
Normal 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>;
|
||||||
|
};
|
||||||
@ -1,9 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
import SerialProvider from '$lib/serial/serial.providers.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||||
|
|
||||||
|
<SerialProvider>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
</SerialProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user