mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-02-15 13:42:42 +00:00
feat: force web usb
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
import { derived, get, writable } from "svelte/store";
|
||||
import { CharaDevice, type SerialPortLike } from "$lib/serial/device";
|
||||
import type { Chord } from "$lib/serial/chord";
|
||||
import type { Writable } from "svelte/store";
|
||||
@@ -7,12 +7,27 @@ import { persistentWritable } from "$lib/storage";
|
||||
import { userPreferences } from "$lib/preferences";
|
||||
import { getMeta } from "$lib/meta/meta-storage";
|
||||
import type { VersionMeta } from "$lib/meta/types/meta";
|
||||
import { serial as serialPolyfill } from "web-serial-polyfill";
|
||||
|
||||
export const serialPort = writable<CharaDevice | undefined>();
|
||||
|
||||
navigator.serial?.addEventListener("disconnect", async (event) => {
|
||||
export const forceWebUSB = persistentWritable("force-webusb", false);
|
||||
|
||||
async function onSerialDisconnect() {
|
||||
serialPort.set(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
export const serialObject = derived<typeof forceWebUSB, Serial>(
|
||||
forceWebUSB,
|
||||
(forceWebUSB) =>
|
||||
forceWebUSB || !("serial" in navigator)
|
||||
? (serialPolyfill as any as Serial)
|
||||
: navigator.serial,
|
||||
);
|
||||
|
||||
if ("serial" in navigator) {
|
||||
navigator.serial.addEventListener("disconnect", onSerialDisconnect);
|
||||
}
|
||||
|
||||
export interface SerialLogEntry {
|
||||
type: "input" | "output" | "system";
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
stringifyChordActions,
|
||||
stringifyPhrase,
|
||||
} from "$lib/serial/chord";
|
||||
import { browser } from "$app/environment";
|
||||
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
||||
import semverGte from "semver/functions/gte";
|
||||
|
||||
@@ -71,23 +70,8 @@ const KEY_COUNTS = {
|
||||
ZERO: 256,
|
||||
} as const;
|
||||
|
||||
if (
|
||||
browser &&
|
||||
navigator.serial === undefined &&
|
||||
import.meta.env.TAURI_FAMILY !== undefined
|
||||
) {
|
||||
await import("./tauri-serial");
|
||||
}
|
||||
|
||||
if (browser && navigator.serial === undefined && navigator.usb !== undefined) {
|
||||
// @ts-expect-error polyfill
|
||||
navigator.serial = await import("web-serial-polyfill").then(
|
||||
({ serial }) => serial,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getViablePorts(): Promise<SerialPort[]> {
|
||||
return navigator.serial.getPorts().then((ports) =>
|
||||
export async function getViablePorts(serial: Serial): Promise<SerialPort[]> {
|
||||
return serial.getPorts().then((ports) =>
|
||||
ports.filter((it) => {
|
||||
const { usbProductId, usbVendorId } = it.getInfo();
|
||||
for (const filter of PORT_FILTERS.values()) {
|
||||
@@ -109,8 +93,8 @@ type LengthArray<T, N extends number, R extends T[] = []> = number extends N
|
||||
? R
|
||||
: LengthArray<T, N, [T, ...R]>;
|
||||
|
||||
export async function canAutoConnect() {
|
||||
return getViablePorts().then((it) => it.length === 1);
|
||||
export async function canAutoConnect(serial: Serial) {
|
||||
return getViablePorts(serial).then((it) => it.length === 1);
|
||||
}
|
||||
|
||||
async function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
themeFromSourceColor,
|
||||
} from "@material/material-color-utilities";
|
||||
import { canAutoConnect, getViablePorts } from "$lib/serial/device";
|
||||
import { initSerial } from "$lib/serial/connection";
|
||||
import { initSerial, serialObject } from "$lib/serial/connection";
|
||||
import type { LayoutData } from "./$types";
|
||||
import { browser } from "$app/environment";
|
||||
import "tippy.js/animations/shift-away.css";
|
||||
@@ -74,8 +74,12 @@
|
||||
webManifestLink = await initPwa();
|
||||
}
|
||||
|
||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
|
||||
const [port] = await getViablePorts();
|
||||
if (
|
||||
browser &&
|
||||
$userPreferences.autoConnect &&
|
||||
(await canAutoConnect($serialObject))
|
||||
) {
|
||||
const [port] = await getViablePorts($serialObject);
|
||||
await initSerial(port!, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
<script lang="ts">
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import { preference, userPreferences } from "$lib/preferences";
|
||||
import { initSerial } from "$lib/serial/connection";
|
||||
import {
|
||||
forceWebUSB,
|
||||
initSerial,
|
||||
serialObject,
|
||||
} from "$lib/serial/connection";
|
||||
import {
|
||||
getPortName,
|
||||
PORT_FILTERS,
|
||||
type SerialPortLike,
|
||||
} from "$lib/serial/device";
|
||||
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
||||
import { onMount } from "svelte";
|
||||
import { persistentWritable } from "$lib/storage";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
let ports = $state<SerialPort[]>([]);
|
||||
let element: HTMLDivElement | undefined = $state();
|
||||
let supportsWebSerial = browser && "serial" in navigator;
|
||||
let supportsWebUSB = browser && "usb" in navigator;
|
||||
|
||||
onMount(() => {
|
||||
refreshPorts();
|
||||
$effect(() => {
|
||||
refreshPorts($serialObject);
|
||||
});
|
||||
|
||||
let hasDiscoveredAutoConnect = persistentWritable(
|
||||
@@ -29,8 +35,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshPorts() {
|
||||
ports = await navigator.serial.getPorts();
|
||||
async function refreshPorts(serial: Serial) {
|
||||
ports = await serial.getPorts();
|
||||
}
|
||||
|
||||
async function connect(port: SerialPortLike, withSync: boolean) {
|
||||
@@ -46,13 +52,13 @@
|
||||
element?.closest<HTMLElement>("[popover]")?.hidePopover();
|
||||
}
|
||||
|
||||
async function connectDevice(event: MouseEvent) {
|
||||
const port = await navigator.serial.requestPort({
|
||||
async function connectDevice(event: MouseEvent, serial: Serial) {
|
||||
const port = await serial.requestPort({
|
||||
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
|
||||
});
|
||||
if (!port) return;
|
||||
closePopover();
|
||||
refreshPorts();
|
||||
refreshPorts(serial);
|
||||
connect(port, true);
|
||||
}
|
||||
</script>
|
||||
@@ -60,55 +66,79 @@
|
||||
<div
|
||||
bind:this={element}
|
||||
class="device-list"
|
||||
onmouseenter={() => refreshPorts()}
|
||||
onmouseenter={() => refreshPorts($serialObject)}
|
||||
role="region"
|
||||
>
|
||||
{#if ports.length === 1}
|
||||
{#if supportsWebSerial || supportsWebUSB}
|
||||
<fieldset class:promote={!$hasDiscoveredAutoConnect}>
|
||||
<label
|
||||
><input type="checkbox" use:preference={"autoConnect"} />
|
||||
<div class="title">{$LL.deviceManager.AUTO_CONNECT()}</div>
|
||||
</label>
|
||||
{#if ports.length === 1}
|
||||
<label
|
||||
><input type="checkbox" use:preference={"autoConnect"} />
|
||||
<div class="title">{$LL.deviceManager.AUTO_CONNECT()}</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
><input type="checkbox" use:preference={"backup"} />
|
||||
<div class="title">{@html $LL.backup.AUTO_BACKUP()}</div>
|
||||
<label
|
||||
><input type="checkbox" use:preference={"backup"} />
|
||||
<div class="title">{@html $LL.backup.AUTO_BACKUP()}</div>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<label title="You can try this if you have trouble with the connection."
|
||||
><input
|
||||
type="checkbox"
|
||||
disabled={!supportsWebSerial}
|
||||
checked={!supportsWebSerial || $forceWebUSB}
|
||||
onchange={(event) => {
|
||||
$forceWebUSB = (event.target as HTMLInputElement).checked;
|
||||
}}
|
||||
/>
|
||||
<div class="title">WebUSB Fallback</div>
|
||||
</label>
|
||||
</fieldset>
|
||||
{/if}
|
||||
{#if ports.length !== 0}
|
||||
<h4>Recent Devices</h4>
|
||||
<div class="devices">
|
||||
<!--
|
||||
{#if ports.length !== 0}
|
||||
<h4>Recent Devices</h4>
|
||||
<div class="devices">
|
||||
<!--
|
||||
<div class="device">
|
||||
<button onclick={connectCC0}> CC0</button>
|
||||
</div>-->
|
||||
{#each ports as port}
|
||||
<div class="device">
|
||||
<button
|
||||
onclick={(event) => {
|
||||
connect(port, !event.shiftKey);
|
||||
}}
|
||||
>
|
||||
{getPortName(port)}</button
|
||||
>
|
||||
<button
|
||||
class="error"
|
||||
onclick={() => {
|
||||
port.forget();
|
||||
refreshPorts();
|
||||
}}><span class="icon">visibility_off</span> Hide</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
{#each ports as port}
|
||||
<div class="device">
|
||||
<button
|
||||
onclick={(event) => {
|
||||
connect(port, !event.shiftKey);
|
||||
}}
|
||||
>
|
||||
{getPortName(port)}</button
|
||||
>
|
||||
<button
|
||||
class="error"
|
||||
onclick={() => {
|
||||
port.forget();
|
||||
refreshPorts($serialObject);
|
||||
}}><span class="icon">visibility_off</span> Hide</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pair">
|
||||
<button
|
||||
onclick={(event) => connectDevice(event, $serialObject)}
|
||||
class="primary"><span class="icon">add</span>Connect</button
|
||||
>
|
||||
<!--<a href="/ccos/zero_wasm/"><span class="icon">add</span>Virtual Device</a>-->
|
||||
</div>
|
||||
{#if !supportsWebSerial}
|
||||
<p>Browser with limited support detected.</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p><b>Your browser is missing support for critical features.</b></p>
|
||||
<p>
|
||||
Please use a Chromium-based browser such as Chrome, Edge or Chromium
|
||||
instead.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="pair">
|
||||
<button onclick={connectDevice} class="primary"
|
||||
><span class="icon">add</span>Connect</button
|
||||
>
|
||||
<!--<a href="/ccos/zero_wasm/"><span class="icon">add</span>Virtual Device</a>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { downloadBackup } from "$lib/backup/backup";
|
||||
import { initSerial, serialPort } from "$lib/serial/connection";
|
||||
import { initSerial, serialObject, serialPort } from "$lib/serial/connection";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import { lt as semverLt } from "semver";
|
||||
import type { LoaderOptions, ESPLoader } from "esptool-js";
|
||||
@@ -95,9 +95,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
async function connect(serial: Serial) {
|
||||
try {
|
||||
const port = await navigator.serial.requestPort();
|
||||
const port = await serial.requestPort();
|
||||
await initSerial(port!, true);
|
||||
step = 1;
|
||||
} catch (e) {
|
||||
@@ -138,9 +138,9 @@
|
||||
step = 4;
|
||||
}
|
||||
|
||||
async function espBootloader() {
|
||||
async function espBootloader(serial: Serial) {
|
||||
$serialPort?.forget();
|
||||
const port = await navigator.serial.requestPort();
|
||||
const port = await serial.requestPort();
|
||||
port.open({ baudRate: 1200 });
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@
|
||||
return espLoader;
|
||||
}
|
||||
|
||||
async function flashImages() {
|
||||
const port = await navigator.serial.requestPort();
|
||||
async function flashImages(serial: Serial) {
|
||||
const port = await serial.requestPort();
|
||||
try {
|
||||
const esptool = data.meta.update.esptool!;
|
||||
espLoader = await connectEsp(port);
|
||||
@@ -202,8 +202,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function eraseSPI() {
|
||||
const port = await navigator.serial.requestPort();
|
||||
async function eraseSPI(serial: Serial) {
|
||||
const port = await serial.requestPort();
|
||||
try {
|
||||
console.log(data.meta);
|
||||
const spiFlash = data.meta.spiFlash!;
|
||||
@@ -314,7 +314,7 @@
|
||||
<section>
|
||||
<ol>
|
||||
<li>
|
||||
<button class="inline-button" onclick={connect}
|
||||
<button class="inline-button" onclick={() => connect($serialObject)}
|
||||
><span class="icon">usb</span>Connect</button
|
||||
>
|
||||
your device
|
||||
@@ -367,17 +367,17 @@
|
||||
</p>
|
||||
|
||||
<div class="esp-buttons">
|
||||
<button onclick={espBootloader}
|
||||
<button onclick={() => espBootloader($serialObject)}
|
||||
><span class="icon">memory</span>ESP Bootloader</button
|
||||
>
|
||||
<button onclick={flashImages}
|
||||
<button onclick={() => flashImages($serialObject)}
|
||||
><span class="icon">developer_board</span>Flash Images</button
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" id="erase" bind:checked={eraseAll} />Erase
|
||||
All</label
|
||||
>
|
||||
<button onclick={eraseSPI}
|
||||
<button onclick={() => eraseSPI($serialObject)}
|
||||
><span class="icon">developer_board</span>Erase SPI Flash</button
|
||||
>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user