mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-22 01:42:47 +00:00
feat: new connection flow
This commit is contained in:
@@ -27,6 +27,7 @@ const config: IconsConfig = {
|
|||||||
"sync",
|
"sync",
|
||||||
"restart_alt",
|
"restart_alt",
|
||||||
"usb",
|
"usb",
|
||||||
|
"usb_off",
|
||||||
"rule_settings",
|
"rule_settings",
|
||||||
"123",
|
"123",
|
||||||
"abc",
|
"abc",
|
||||||
@@ -34,6 +35,7 @@ const config: IconsConfig = {
|
|||||||
"cloud_done",
|
"cloud_done",
|
||||||
"backup",
|
"backup",
|
||||||
"cloud_download",
|
"cloud_download",
|
||||||
|
"cloud_off",
|
||||||
"share",
|
"share",
|
||||||
"ios_share",
|
"ios_share",
|
||||||
"close",
|
"close",
|
||||||
@@ -41,6 +43,14 @@ const config: IconsConfig = {
|
|||||||
"arrow_back_ios_new",
|
"arrow_back_ios_new",
|
||||||
"save",
|
"save",
|
||||||
"settings_backup_restore",
|
"settings_backup_restore",
|
||||||
|
"sort",
|
||||||
|
"filter",
|
||||||
|
"settings_power",
|
||||||
|
"link",
|
||||||
|
"link_off",
|
||||||
|
"chevron_right",
|
||||||
|
"check_circle",
|
||||||
|
"error",
|
||||||
],
|
],
|
||||||
codePoints: {
|
codePoints: {
|
||||||
speed: "e9e4",
|
speed: "e9e4",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const popup: Action<HTMLButtonElement, ComponentType> = (node, Component)
|
|||||||
const edit = tippy(node, {
|
const edit = tippy(node, {
|
||||||
interactive: true,
|
interactive: true,
|
||||||
trigger: "click",
|
trigger: "click",
|
||||||
|
sticky: true,
|
||||||
onShow(instance) {
|
onShow(instance) {
|
||||||
target = instance.popper.querySelector(".tippy-content") as HTMLElement
|
target = instance.popper.querySelector(".tippy-content") as HTMLElement
|
||||||
target.classList.add("active")
|
target.classList.add("active")
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import type {Action} from "svelte/action"
|
|||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
backup: boolean
|
backup: boolean
|
||||||
autoSync: boolean
|
autoConnect: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userPreferences = writable<UserPreferences>({
|
export const userPreferences = writable<UserPreferences>({
|
||||||
backup: false,
|
backup: false,
|
||||||
autoSync: true,
|
autoConnect: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const preference: Action<HTMLInputElement, keyof UserPreferences> = (node, key) => {
|
export const preference: Action<HTMLInputElement, keyof UserPreferences> = (node, key) => {
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"
|
|||||||
|
|
||||||
let device: CharaDevice // @hmr:keep
|
let device: CharaDevice // @hmr:keep
|
||||||
|
|
||||||
export async function initSerial() {
|
export async function initSerial(manual = false) {
|
||||||
const device = get(serialPort) ?? new CharaDevice()
|
const device = get(serialPort) ?? new CharaDevice()
|
||||||
await device.ready()
|
await device.init(manual)
|
||||||
serialPort.set(device)
|
serialPort.set(device)
|
||||||
|
|
||||||
syncStatus.set("downloading")
|
syncStatus.set("downloading")
|
||||||
|
|||||||
@@ -14,74 +14,71 @@ export async function canAutoConnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CharaDevice {
|
export class CharaDevice {
|
||||||
private readonly port: Promise<SerialPort>
|
private port!: SerialPort
|
||||||
private readonly reader: Promise<ReadableStreamDefaultReader<string>>
|
private reader!: ReadableStreamDefaultReader<string>
|
||||||
|
|
||||||
private readonly abortController1 = new AbortController()
|
private readonly abortController1 = new AbortController()
|
||||||
private readonly abortController2 = new AbortController()
|
private readonly abortController2 = new AbortController()
|
||||||
|
|
||||||
|
private streamClosed!: Promise<void>
|
||||||
|
|
||||||
private lock?: Promise<true>
|
private lock?: Promise<true>
|
||||||
|
|
||||||
version: Promise<string>
|
version!: string
|
||||||
deviceId: Promise<string>
|
deviceId!: string
|
||||||
|
|
||||||
constructor(baudRate = 115200) {
|
constructor(private readonly baudRate = 115200) {}
|
||||||
this.port = getViablePorts().then(async ports => {
|
|
||||||
const port =
|
async init(manual = false) {
|
||||||
ports.length === 1
|
const ports = await getViablePorts()
|
||||||
? ports[0]
|
this.port =
|
||||||
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
|
!manual && ports.length === 1
|
||||||
await port.open({baudRate})
|
? ports[0]
|
||||||
const info = port.getInfo()
|
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
|
||||||
serialLog.update(it => {
|
await this.port.open({baudRate: this.baudRate})
|
||||||
it.push({
|
const info = this.port.getInfo()
|
||||||
type: "system",
|
serialLog.update(it => {
|
||||||
value: `Connected; ID: 0x${info.usbProductId?.toString(16)}; Vendor: 0x${info.usbVendorId?.toString(
|
it.push({
|
||||||
16,
|
type: "system",
|
||||||
)}`,
|
value: `Connected; ID: 0x${info.usbProductId?.toString(16)}; Vendor: 0x${info.usbVendorId?.toString(
|
||||||
})
|
16,
|
||||||
return it
|
)}`,
|
||||||
})
|
})
|
||||||
return port
|
return it
|
||||||
})
|
})
|
||||||
this.reader = this.port.then(async port => {
|
|
||||||
const decoderStream = new TextDecoderStream()
|
|
||||||
void port.readable!.pipeTo(decoderStream.writable, {signal: this.abortController1.signal})
|
|
||||||
|
|
||||||
return decoderStream
|
const decoderStream = new TextDecoderStream()
|
||||||
.readable!.pipeThrough(new TransformStream(new LineBreakTransformer()), {
|
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
|
||||||
signal: this.abortController2.signal,
|
signal: this.abortController1.signal,
|
||||||
})
|
|
||||||
.getReader()
|
|
||||||
})
|
})
|
||||||
this.lock = this.reader.then(() => {
|
|
||||||
delete this.lock
|
this.reader = decoderStream
|
||||||
return true
|
.readable!.pipeThrough(new TransformStream(new LineBreakTransformer()), {
|
||||||
})
|
signal: this.abortController2.signal,
|
||||||
this.version = this.send("VERSION")
|
})
|
||||||
this.deviceId = this.send("ID")
|
.getReader()
|
||||||
|
|
||||||
|
this.version = await this.send("VERSION")
|
||||||
|
this.deviceId = await this.send("ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
private async internalRead() {
|
private async internalRead() {
|
||||||
return this.reader.then(async it => {
|
const {value} = await this.reader.read()
|
||||||
const result: string = await it.read().then(({value}) => value!)
|
serialLog.update(it => {
|
||||||
serialLog.update(it => {
|
it.push({
|
||||||
it.push({
|
type: "output",
|
||||||
type: "output",
|
value: value!,
|
||||||
value: result,
|
|
||||||
})
|
|
||||||
return it
|
|
||||||
})
|
})
|
||||||
return result
|
return it
|
||||||
})
|
})
|
||||||
|
return value!
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a command to the device
|
* Send a command to the device
|
||||||
*/
|
*/
|
||||||
private async internalSend(...command: string[]) {
|
private async internalSend(...command: string[]) {
|
||||||
const port = await this.port
|
const writer = this.port.writable!.getWriter()
|
||||||
const writer = port.writable!.getWriter()
|
|
||||||
try {
|
try {
|
||||||
serialLog.update(it => {
|
serialLog.update(it => {
|
||||||
it.push({
|
it.push({
|
||||||
@@ -96,19 +93,18 @@ export class CharaDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready() {
|
|
||||||
await this.port
|
|
||||||
}
|
|
||||||
|
|
||||||
async forget() {
|
async forget() {
|
||||||
await (await this.port).forget()
|
await this.disconnect()
|
||||||
|
await this.port.forget()
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
this.abortController1.abort()
|
await this.reader.cancel()
|
||||||
this.abortController2.abort()
|
await this.streamClosed.catch(() => {
|
||||||
;(await this.reader).releaseLock()
|
/** noop */
|
||||||
await (await this.port).close()
|
})
|
||||||
|
this.reader.releaseLock()
|
||||||
|
await this.port.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
$padding: 16px;
|
$padding: 16px;
|
||||||
|
|
||||||
.tippy-box[data-theme~="surface-variant"] {
|
.tippy-box[data-theme~="surface-variant"] {
|
||||||
|
// overflow: hidden;
|
||||||
|
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
|
||||||
background-color: var(--md-sys-color-surface-variant);
|
background-color: var(--md-sys-color-surface-variant);
|
||||||
filter: drop-shadow(0 0 12px #000a);
|
filter: drop-shadow(0 0 12px #000a);
|
||||||
border-radius: calc(24px + $padding);
|
border-radius: calc(24px + $padding);
|
||||||
@@ -11,10 +14,10 @@ $padding: 16px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin-block-start: 8px;
|
|
||||||
margin-block-end: calc(8px + $padding);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-block-start: 8px;
|
||||||
|
margin-block-end: calc(8px + $padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
@each $placement in top, bottom, right, left {
|
@each $placement in top, bottom, right, left {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
} satisfies RegisterSWOptions)
|
} satisfies RegisterSWOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($userPreferences.autoSync && (await canAutoConnect())) await initSerial()
|
if ($userPreferences.autoConnect && (await canAutoConnect())) await initSerial()
|
||||||
})
|
})
|
||||||
|
|
||||||
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : ""
|
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : ""
|
||||||
|
|||||||
@@ -1,52 +1,159 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import {initSerial, serialPort} from "$lib/serial/connection"
|
import {initSerial, serialPort} from "$lib/serial/connection"
|
||||||
import {browser} from "$app/environment"
|
import {browser} from "$app/environment"
|
||||||
import {getViablePorts} from "$lib/serial/device"
|
import {getViablePorts} from "$lib/serial/device"
|
||||||
|
import {slide, fade} from "svelte/transition"
|
||||||
|
import {preference} from "$lib/preferences"
|
||||||
|
|
||||||
|
let connectDialog = false
|
||||||
|
let powerDialog = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h2>Devices</h2>
|
<section>
|
||||||
|
<h2>Device</h2>
|
||||||
|
|
||||||
<div class="row">
|
{#if $serialPort}
|
||||||
<button disabled={$serialPort === undefined}><span class="icon">restart_alt</span>Reboot</button>
|
<p transition:slide>
|
||||||
<button disabled={$serialPort === undefined}><span class="icon">rule_settings</span>Bootloader</button>
|
{$serialPort.deviceId}
|
||||||
</div>
|
<br />
|
||||||
{#if browser}
|
Version {$serialPort.version}
|
||||||
{#await ($serialPort, getViablePorts()) then ports}
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if browser}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{#if ports.length === 0}
|
<button
|
||||||
<button class="secondary" on:click={initSerial}>
|
class:secondary={$serialPort}
|
||||||
<span class="icon">usb</span>Pair
|
class:error={!$serialPort}
|
||||||
</button>
|
on:click={() => (connectDialog = !connectDialog)}
|
||||||
{:else if $serialPort}
|
>
|
||||||
<button
|
{#if $serialPort}
|
||||||
class="secondary"
|
<span class="icon">usb</span>
|
||||||
on:click={() => {
|
{:else}
|
||||||
$serialPort.forget()
|
<span class="icon">error</span>
|
||||||
$serialPort = undefined
|
{/if}
|
||||||
}}><span class="icon">usb</span>Unpair</button
|
Connection
|
||||||
>
|
<span class="icon">chevron_right</span>
|
||||||
{:else}
|
</button>
|
||||||
<button class="secondary" on:click={initSerial}><span class="icon">usb</span>Connect</button>
|
<button class="icon" disabled={$serialPort === undefined} on:click={() => (powerDialog = !powerDialog)}
|
||||||
{/if}
|
>settings_power</button
|
||||||
{#if $serialPort}
|
>
|
||||||
<button
|
|
||||||
on:click={() => {
|
|
||||||
$serialPort.disconnect()
|
|
||||||
$serialPort = undefined
|
|
||||||
}}><span class="icon">usb</span>Disconnect</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{#await ($serialPort, getViablePorts()) then ports}
|
||||||
{/if}
|
{#if connectDialog}
|
||||||
|
<div
|
||||||
|
class="backdrop"
|
||||||
|
transition:fade={{duration: 250}}
|
||||||
|
on:click={() => (connectDialog = !connectDialog)}
|
||||||
|
/>
|
||||||
|
<dialog open transition:slide={{duration: 250}}>
|
||||||
|
<label><input type="checkbox" use:preference={"autoConnect"} />Auto Connect</label>
|
||||||
|
{#if $serialPort}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
$serialPort.disconnect()
|
||||||
|
$serialPort = undefined
|
||||||
|
}}><span class="icon">usb_off</span>Disconnect</button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
{#if ports.length > 0}
|
||||||
|
<button on:click={() => initSerial()}><span class="icon">usb</span>Connect</button>
|
||||||
|
{/if}
|
||||||
|
<button on:click={() => initSerial(true)}><span class="icon">link</span>Pair Device</button>
|
||||||
|
{/if}
|
||||||
|
{#if $serialPort}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
$serialPort.forget()
|
||||||
|
$serialPort = undefined
|
||||||
|
}}><span class="icon">link_off</span>Unpair Device</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</dialog>
|
||||||
|
{/if}
|
||||||
|
{/await}
|
||||||
|
{#if powerDialog}
|
||||||
|
<div class="backdrop" transition:fade={{duration: 250}} on:click={() => (powerDialog = !powerDialog)} />
|
||||||
|
<dialog open transition:slide={{duration: 250}}>
|
||||||
|
<h3>Boot Menu</h3>
|
||||||
|
<button><span class="icon">restart_alt</span>Reboot</button>
|
||||||
|
<button><span class="icon">rule_settings</span>Bootloader</button>
|
||||||
|
</dialog>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
h2 {
|
||||||
|
margin-block: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-block: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
inset: 0;
|
||||||
|
|
||||||
|
background: #0005;
|
||||||
|
border-radius: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
margin-block-start: 16px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
color: var(--md-sys-color-on-secondary-container);
|
||||||
|
|
||||||
|
background: var(--md-sys-color-secondary-container);
|
||||||
|
border: none;
|
||||||
|
border-radius: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog > * {
|
||||||
|
margin-inline: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog > :first-child {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding-block: 8px;
|
||||||
|
|
||||||
|
color: var(--md-sys-color-on-secondary);
|
||||||
|
|
||||||
|
background: var(--md-sys-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@@ -85,6 +192,11 @@
|
|||||||
background: var(--md-sys-color-secondary);
|
background: var(--md-sys-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: var(--md-sys-color-on-error);
|
||||||
|
background: var(--md-sys-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
&:active:not(:disabled) {
|
&:active:not(:disabled) {
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
background: var(--md-sys-color-surface-variant);
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import ConnectionPopup from "./ConnectionPopup.svelte"
|
import ConnectionPopup from "./ConnectionPopup.svelte"
|
||||||
import {canAutoConnect} from "$lib/serial/device"
|
import {canAutoConnect} from "$lib/serial/device"
|
||||||
import {browser} from "$app/environment"
|
import {browser} from "$app/environment"
|
||||||
|
import {userPreferences} from "$lib/preferences"
|
||||||
|
|
||||||
const training = [
|
const training = [
|
||||||
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
||||||
@@ -53,8 +54,10 @@
|
|||||||
backup
|
backup
|
||||||
{:else if $syncStatus === "uploading"}
|
{:else if $syncStatus === "uploading"}
|
||||||
cloud_download
|
cloud_download
|
||||||
{:else}
|
{:else if $userPreferences.backup}
|
||||||
cloud_done
|
cloud_done
|
||||||
|
{:else}
|
||||||
|
cloud_off
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user