feat: tauri serial polyfill

This commit is contained in:
2023-08-04 00:08:28 +02:00
parent 9c1918e683
commit 42922e7ce0
23 changed files with 660 additions and 486 deletions

View File

@@ -7,8 +7,6 @@
export let results: number[] = []
export let width: number
console.log(width)
</script>
<div class="list" style="width: {width}px">

View File

@@ -8,7 +8,6 @@ export const popup: Action<HTMLButtonElement, ComponentType> = (node, Component)
const edit = tippy(node, {
interactive: true,
trigger: "click",
sticky: true,
onShow(instance) {
target = instance.popper.querySelector(".tippy-content") as HTMLElement
target.classList.add("active")

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import {createEventDispatcher} from "svelte"
export let ports: SerialPort[]
const dispatch = createEventDispatcher<{confirm: SerialPort | undefined}>()
let selected = ports[0].getInfo().name
</script>
<dialog>
{#each ports as port}
{@const info = port.getInfo()}
<label>{info.product}<input type="radio" name="port" value={info.name} bind:group={selected} /></label>
{/each}
<button on:click={() => dispatch("confirm", undefined)}>Cancel</button>
<button
on:click={() =>
dispatch(
"confirm",
ports.find(it => it.getInfo().name === selected),
)}>Ok</button
>
</dialog>

View File

@@ -6,7 +6,7 @@ import type {CharaLayout} from "$lib/serialization/layout"
import {persistentWritable} from "$lib/storage"
import {userPreferences} from "$lib/preferences"
export const serialPort = writable<CharaDevice>()
export const serialPort = writable<CharaDevice | undefined>()
export interface SerialLogEntry {
type: "input" | "output" | "system"

View File

@@ -11,7 +11,6 @@ if (browser && import.meta.env.TAURI_FAMILY !== undefined) {
}
export async function getViablePorts(): Promise<SerialPort[]> {
console.log(await navigator.serial.getPorts().then(it => it.map(it => it.getInfo())))
return navigator.serial.getPorts().then(ports => ports.filter(it => it.getInfo().usbVendorId === VENDOR_ID))
}

View File

@@ -0,0 +1,8 @@
/// <references types="@types/w3c-web-serial" />
interface SerialPortInfo {
name?: string
serialNumber?: string
manufacturer?: string
product?: string
}

View File

@@ -1,22 +1,65 @@
import {invoke} from "@tauri-apps/api"
import TauriSerialDialog from "$lib/serial/TauriSerialDialog.svelte"
export type TauriSerialPort = Pick<
SerialPort,
"getInfo" | "open" | "close" | "readable" | "writable" | "forget"
>
function NativeSerialPort(info: SerialPortInfo): TauriSerialPort {
return {
getInfo() {
return info
},
async open({baudRate}: SerialOptions) {
await invoke("plugin:serial|open", {path: info.name, baudRate})
},
async close() {
await invoke("plugin:serial|close", {path: info.name})
},
async forget() {
// noop
},
readable: new ReadableStream({
async pull(controller) {
const result = await invoke<number[]>("plugin:serial|read", {path: info.name})
controller.enqueue(new Uint8Array(result))
},
}),
writable: new WritableStream({
async write(chunk) {
await invoke("plugin:serial|write", {path: info.name, chunk: Array.from(chunk)})
},
}),
}
}
// @ts-expect-error polyfill
// noinspection JSConstantReassignment
navigator.serial = {
getPorts(): Promise<SerialPort[]> {
async getPorts(): Promise<SerialPort[]> {
return invoke<any[]>("plugin:serial|get_serial_ports").then(ports =>
ports.map<Partial<SerialPort>>(port => ({
getInfo() {
return {
name: port["name"],
usbVendorId: port["vendor_id"],
usbProductId: port["product_id"],
serialNumber: port["serial_number"],
manufacturer: port["manufacturer"],
product: port["product"],
} as SerialPortInfo
},
})),
ports.map(NativeSerialPort),
) as Promise<SerialPort[]>
},
async requestPort(options?: SerialPortRequestOptions): Promise<SerialPort> {
const ports = await navigator.serial.getPorts().then(ports =>
options?.filters !== undefined
? ports.filter(port =>
options.filters!.some(({usbVendorId, usbProductId}) => {
const info = port.getInfo()
return (
(usbVendorId === undefined || info.usbVendorId === usbVendorId) &&
(usbProductId === undefined || info.usbProductId === usbProductId)
)
}),
)
: ports,
)
const dialog = new TauriSerialDialog({target: document.body, props: {ports}})
const port = await new Promise<SerialPort>(resolve => dialog.$on("confirm", resolve))
dialog.$destroy()
return port
},
}

View File

@@ -9,9 +9,7 @@
import Navigation from "./Navigation.svelte"
import {canAutoConnect} from "$lib/serial/device"
import {initSerial} from "$lib/serial/connection"
import {pwaInfo} from "virtual:pwa-info"
import type {LayoutServerData} from "./$types"
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
import {browser} from "$app/environment"
import BrowserWarning from "./BrowserWarning.svelte"
import "tippy.js/animations/shift-away.css"
@@ -46,21 +44,15 @@
const dark = it.mode === "dark" // window.matchMedia("(prefers-color-scheme: dark)").matches
applyTheme(theme, {target: document.body, dark})
})
if (pwaInfo) {
const {registerSW} = await import("virtual:pwa-register")
registerSW({
immediate: true,
onRegisterError(error) {
console.log("ServiceWorker Registration Error", error)
},
} satisfies RegisterSWOptions)
if (import.meta.env.TAURI_FAMILY === undefined) {
const {initPwa} = await import("./pwa-setup")
await initPwa()
}
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) await initSerial()
})
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : ""
let webManifestLink = ""
</script>
<svelte:head>
@@ -76,7 +68,7 @@
<slot />
</main>
{#if !import.meta.env.TAURI_FAMILY && browser && !("serial" in navigator)}
{#if import.meta.env.TAURI_FAMILY === undefined && browser && !("serial" in navigator)}
<BrowserWarning />
{/if}

View File

@@ -31,7 +31,7 @@
<button
class="secondary"
on:click={() => {
$serialPort.forget()
$serialPort?.forget()
$serialPort = undefined
}}><span class="icon">usb_off</span>{$LL.deviceManager.DISCONNECT()}</button
>
@@ -45,7 +45,7 @@
href="/terminal"
title={$LL.deviceManager.TERMINAL()}
class="icon"
disabled={$serialPort === undefined}
class:disabled={$serialPort === undefined}
on:click={() => (terminal = !terminal)}>terminal</a
>
<button
@@ -62,13 +62,13 @@
<h3>{$LL.deviceManager.bootMenu.TITLE()}</h3>
<button
on:click={() => {
$serialPort.reboot()
$serialPort?.reboot()
$serialPort = undefined
}}><span class="icon">restart_alt</span>{$LL.deviceManager.bootMenu.REBOOT()}</button
>
<button
on:click={() => {
$serialPort.bootloader()
$serialPort?.bootloader()
$serialPort = undefined
}}><span class="icon">rule_settings</span>{$LL.deviceManager.bootMenu.BOOTLOADER()}</button
>
@@ -176,31 +176,22 @@
transition: all 250ms ease;
&:disabled {
cursor: default;
opacity: 0.5;
}
&.icon {
aspect-ratio: 1;
padding-inline-end: 8px;
font-size: 24px;
border-radius: 50%;
}
}
&.secondary {
color: var(--md-sys-color-on-secondary);
background: var(--md-sys-color-secondary);
}
a.disabled,
button:disabled {
cursor: default;
opacity: 0.5;
}
&.error {
color: var(--md-sys-color-on-error);
background: var(--md-sys-color-error);
}
&:active:not(:disabled) {
color: var(--md-sys-color-on-surface-variant);
background: var(--md-sys-color-surface-variant);
}
button:active:not(:disabled) {
color: var(--md-sys-color-on-surface-variant);
background: var(--md-sys-color-surface-variant);
}
</style>

View File

@@ -47,9 +47,11 @@
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button>
<div transition:slide class="separator" />
{/if}
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
<PwaStatus />
{/await}
{#if import.meta.env.TAURI_FAMILY === undefined}
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
<PwaStatus />
{/await}
{/if}
{#if $serialPort}
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}">
{#if $syncStatus === "downloading"}

16
src/routes/pwa-setup.ts Normal file
View File

@@ -0,0 +1,16 @@
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
export async function initPwa(): Promise<string> {
// @ts-expect-error confused TS
const {pwaInfo} = await import("virtual:pwa-info")
// @ts-expect-error confused TS
const {registerSW} = await import("virtual:pwa-register")
registerSW({
immediate: true,
onRegisterError(error) {
console.log("ServiceWorker Registration Error", error)
},
} satisfies RegisterSWOptions)
return pwaInfo ? pwaInfo.webManifest.linkTag : ""
}