mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-07-27 06:54:46 +00:00
refactor: use standard prettier formatting
This commit is contained in:
@@ -1,15 +1,22 @@
|
||||
<script lang="ts">
|
||||
import {createEventDispatcher} from "svelte"
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let ports: SerialPort[]
|
||||
const dispatch = createEventDispatcher<{confirm: SerialPort | undefined}>()
|
||||
let selected = ports[0].getInfo().name
|
||||
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>
|
||||
<label
|
||||
>{info.product}<input
|
||||
type="radio"
|
||||
name="port"
|
||||
value={info.name}
|
||||
bind:group={selected}
|
||||
/></label
|
||||
>
|
||||
{/each}
|
||||
|
||||
<button on:click={() => dispatch("confirm", undefined)}>Cancel</button>
|
||||
@@ -17,7 +24,7 @@
|
||||
on:click={() =>
|
||||
dispatch(
|
||||
"confirm",
|
||||
ports.find(it => it.getInfo().name === selected),
|
||||
ports.find((it) => it.getInfo().name === selected),
|
||||
)}>Ok</button
|
||||
>
|
||||
</dialog>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
deserializeActions,
|
||||
parseChordActions,
|
||||
@@ -6,43 +6,55 @@ import {
|
||||
serializeActions,
|
||||
stringifyChordActions,
|
||||
stringifyPhrase,
|
||||
} from "./chord"
|
||||
} from "./chord";
|
||||
|
||||
describe("chords", function () {
|
||||
describe("actions", function () {
|
||||
it("should serialize actions", function () {
|
||||
expect(serializeActions([32, 51]).toString(16)).toEqual(0xcc200000000000000000000000000n.toString(16))
|
||||
})
|
||||
expect(serializeActions([32, 51]).toString(16)).toEqual(
|
||||
0xcc200000000000000000000000000n.toString(16),
|
||||
);
|
||||
});
|
||||
|
||||
it("should deserialize actions", function () {
|
||||
expect(deserializeActions(0xcc200000000000000000000000000n)).toEqual([32, 51])
|
||||
})
|
||||
expect(deserializeActions(0xcc200000000000000000000000000n)).toEqual([
|
||||
32, 51,
|
||||
]);
|
||||
});
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
it(`should serialize back-forth ${i} actions`, function () {
|
||||
const actions = Array.from({length: i}).map((_, i) => i + 1)
|
||||
expect(deserializeActions(serializeActions(actions))).toEqual(actions)
|
||||
})
|
||||
const actions = Array.from({ length: i }).map((_, i) => i + 1);
|
||||
expect(deserializeActions(serializeActions(actions))).toEqual(actions);
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
describe("phrase", function () {
|
||||
it("should stringify", function () {
|
||||
expect(stringifyPhrase([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff])).toEqual("206872D4651FFF")
|
||||
})
|
||||
expect(stringifyPhrase([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff])).toEqual(
|
||||
"206872D4651FFF",
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse", function () {
|
||||
expect(parsePhrase("206872D4651FFF")).toEqual([0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff])
|
||||
})
|
||||
})
|
||||
expect(parsePhrase("206872D4651FFF")).toEqual([
|
||||
0x20, 0x68, 0x72, 0xd4, 0x65, 0x1fff,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chord actions", function () {
|
||||
it("should stringify", function () {
|
||||
expect(stringifyChordActions([32, 51])).toEqual("000CC200000000000000000000000000")
|
||||
})
|
||||
expect(stringifyChordActions([32, 51])).toEqual(
|
||||
"000CC200000000000000000000000000",
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse", function () {
|
||||
expect(parseChordActions("000CC200000000000000000000000000")).toEqual([32, 51])
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(parseChordActions("000CC200000000000000000000000000")).toEqual([
|
||||
32, 51,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+18
-17
@@ -1,31 +1,31 @@
|
||||
import {compressActions, decompressActions} from "../serialization/actions"
|
||||
import { compressActions, decompressActions } from "../serialization/actions";
|
||||
|
||||
export interface Chord {
|
||||
actions: number[]
|
||||
phrase: number[]
|
||||
actions: number[];
|
||||
phrase: number[];
|
||||
}
|
||||
|
||||
export function parsePhrase(phrase: string): number[] {
|
||||
return decompressActions(
|
||||
Uint8Array.from({length: phrase.length / 2}).map((_, i) =>
|
||||
Uint8Array.from({ length: phrase.length / 2 }).map((_, i) =>
|
||||
Number.parseInt(phrase.slice(i * 2, i * 2 + 2), 16),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function stringifyPhrase(phrase: number[]): string {
|
||||
return [...compressActions(phrase)]
|
||||
.map(it => it.toString(16).padStart(2, "0"))
|
||||
.map((it) => it.toString(16).padStart(2, "0"))
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
export function parseChordActions(actions: string): number[] {
|
||||
return deserializeActions(BigInt(`0x${actions}`))
|
||||
return deserializeActions(BigInt(`0x${actions}`));
|
||||
}
|
||||
|
||||
export function stringifyChordActions(actions: number[]): string {
|
||||
return serializeActions(actions).toString(16).padStart(32, "0").toUpperCase()
|
||||
return serializeActions(actions).toString(16).padStart(32, "0").toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,23 +34,24 @@ export function stringifyChordActions(actions: number[]): string {
|
||||
* Actions are represented as 10-bit codes, for a maximum of 12 actions
|
||||
*/
|
||||
export function serializeActions(actions: number[]): bigint {
|
||||
let native = 0n
|
||||
let native = 0n;
|
||||
for (let i = 1; i <= actions.length; i++) {
|
||||
native |= BigInt(actions[actions.length - i] & 0x3ff) << BigInt((12 - i) * 10)
|
||||
native |=
|
||||
BigInt(actions[actions.length - i] & 0x3ff) << BigInt((12 - i) * 10);
|
||||
}
|
||||
return native
|
||||
return native;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {serializeActions}
|
||||
*/
|
||||
export function deserializeActions(native: bigint): number[] {
|
||||
const actions = []
|
||||
const actions = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const action = Number(native & 0x3ffn)
|
||||
actions.push(action)
|
||||
native >>= 10n
|
||||
const action = Number(native & 0x3ffn);
|
||||
actions.push(action);
|
||||
native >>= 10n;
|
||||
}
|
||||
|
||||
return actions
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import {get, writable} from "svelte/store"
|
||||
import {CharaDevice} from "$lib/serial/device"
|
||||
import type {Chord} from "$lib/serial/chord"
|
||||
import type {Writable} from "svelte/store"
|
||||
import type {CharaLayout} from "$lib/serialization/layout"
|
||||
import {persistentWritable} from "$lib/storage"
|
||||
import {userPreferences} from "$lib/preferences"
|
||||
import settingInfo from "$lib/assets/settings.yml"
|
||||
import { get, writable } from "svelte/store";
|
||||
import { CharaDevice } from "$lib/serial/device";
|
||||
import type { Chord } from "$lib/serial/chord";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { CharaLayout } from "$lib/serialization/layout";
|
||||
import { persistentWritable } from "$lib/storage";
|
||||
import { userPreferences } from "$lib/preferences";
|
||||
import settingInfo from "$lib/assets/settings.yml";
|
||||
|
||||
export const serialPort = writable<CharaDevice | undefined>()
|
||||
export const serialPort = writable<CharaDevice | undefined>();
|
||||
|
||||
export interface SerialLogEntry {
|
||||
type: "input" | "output" | "system"
|
||||
value: string
|
||||
type: "input" | "output" | "system";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const serialLog = writable<SerialLogEntry[]>([])
|
||||
export const serialLog = writable<SerialLogEntry[]>([]);
|
||||
|
||||
/**
|
||||
* Chords as read from the device
|
||||
@@ -23,7 +23,7 @@ export const deviceChords = persistentWritable<Chord[]>(
|
||||
"chord-library",
|
||||
[],
|
||||
() => get(userPreferences).backup,
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Layout as read from the device
|
||||
@@ -32,7 +32,7 @@ export const deviceLayout = persistentWritable<CharaLayout>(
|
||||
"layout",
|
||||
[[], [], []],
|
||||
() => get(userPreferences).backup,
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Settings as read from the device
|
||||
@@ -41,61 +41,66 @@ export const deviceSettings = persistentWritable<number[]>(
|
||||
"device-settings",
|
||||
[],
|
||||
() => get(userPreferences).backup,
|
||||
)
|
||||
);
|
||||
|
||||
export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done")
|
||||
export const syncStatus: Writable<
|
||||
"done" | "error" | "downloading" | "uploading"
|
||||
> = writable("done");
|
||||
|
||||
export interface ProgressInfo {
|
||||
max: number
|
||||
current: number
|
||||
max: number;
|
||||
current: number;
|
||||
}
|
||||
export const syncProgress = writable<ProgressInfo | undefined>(undefined)
|
||||
export const syncProgress = writable<ProgressInfo | undefined>(undefined);
|
||||
|
||||
export async function initSerial(manual = false) {
|
||||
const device = get(serialPort) ?? new CharaDevice()
|
||||
await device.init(manual)
|
||||
serialPort.set(device)
|
||||
sync()
|
||||
const device = get(serialPort) ?? new CharaDevice();
|
||||
await device.init(manual);
|
||||
serialPort.set(device);
|
||||
sync();
|
||||
}
|
||||
|
||||
export async function sync() {
|
||||
const device = get(serialPort)
|
||||
if (!device) return
|
||||
const chordCount = await device.getChordCount()
|
||||
syncStatus.set("downloading")
|
||||
const device = get(serialPort);
|
||||
if (!device) return;
|
||||
const chordCount = await device.getChordCount();
|
||||
syncStatus.set("downloading");
|
||||
|
||||
const max = Object.keys(settingInfo.settings).length + device.keyCount * 3 + chordCount
|
||||
let current = 0
|
||||
syncProgress.set({max, current})
|
||||
const max =
|
||||
Object.keys(settingInfo.settings).length + device.keyCount * 3 + chordCount;
|
||||
let current = 0;
|
||||
syncProgress.set({ max, current });
|
||||
function progressTick() {
|
||||
current++
|
||||
syncProgress.set({max, current})
|
||||
current++;
|
||||
syncProgress.set({ max, current });
|
||||
}
|
||||
|
||||
const parsedSettings: number[] = []
|
||||
const parsedSettings: number[] = [];
|
||||
for (const key in settingInfo.settings) {
|
||||
try {
|
||||
parsedSettings[Number.parseInt(key)] = await device.getSetting(Number.parseInt(key))
|
||||
parsedSettings[Number.parseInt(key)] = await device.getSetting(
|
||||
Number.parseInt(key),
|
||||
);
|
||||
} catch {}
|
||||
progressTick()
|
||||
progressTick();
|
||||
}
|
||||
deviceSettings.set(parsedSettings)
|
||||
deviceSettings.set(parsedSettings);
|
||||
|
||||
const parsedLayout: CharaLayout = [[], [], []]
|
||||
const parsedLayout: CharaLayout = [[], [], []];
|
||||
for (let layer = 1; layer <= 3; layer++) {
|
||||
for (let i = 0; i < device.keyCount; i++) {
|
||||
parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i)
|
||||
progressTick()
|
||||
parsedLayout[layer - 1][i] = await device.getLayoutKey(layer, i);
|
||||
progressTick();
|
||||
}
|
||||
}
|
||||
deviceLayout.set(parsedLayout)
|
||||
deviceLayout.set(parsedLayout);
|
||||
|
||||
const chordInfo = []
|
||||
const chordInfo = [];
|
||||
for (let i = 0; i < chordCount; i++) {
|
||||
chordInfo.push(await device.getChord(i))
|
||||
progressTick()
|
||||
chordInfo.push(await device.getChord(i));
|
||||
progressTick();
|
||||
}
|
||||
deviceChords.set(chordInfo)
|
||||
syncStatus.set("done")
|
||||
syncProgress.set(undefined)
|
||||
deviceChords.set(chordInfo);
|
||||
syncStatus.set("done");
|
||||
syncProgress.set(undefined);
|
||||
}
|
||||
|
||||
+186
-147
@@ -1,171 +1,190 @@
|
||||
import {LineBreakTransformer} from "$lib/serial/line-break-transformer"
|
||||
import {serialLog} from "$lib/serial/connection"
|
||||
import type {Chord} from "$lib/serial/chord"
|
||||
import {SemVer} from "$lib/serial/sem-ver"
|
||||
import {parseChordActions, parsePhrase, stringifyChordActions, stringifyPhrase} from "$lib/serial/chord"
|
||||
import {browser} from "$app/environment"
|
||||
import { LineBreakTransformer } from "$lib/serial/line-break-transformer";
|
||||
import { serialLog } from "$lib/serial/connection";
|
||||
import type { Chord } from "$lib/serial/chord";
|
||||
import { SemVer } from "$lib/serial/sem-ver";
|
||||
import {
|
||||
parseChordActions,
|
||||
parsePhrase,
|
||||
stringifyChordActions,
|
||||
stringifyPhrase,
|
||||
} from "$lib/serial/chord";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||
["ONE M0", {usbProductId: 32783, usbVendorId: 9114}],
|
||||
["LITE S2", {usbProductId: 33070, usbVendorId: 12346}],
|
||||
["LITE M0", {usbProductId: 32796, usbVendorId: 9114}],
|
||||
["X", {usbProductId: 33163, usbVendorId: 12346}],
|
||||
])
|
||||
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
||||
["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }],
|
||||
["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }],
|
||||
["X", { usbProductId: 33163, usbVendorId: 12346 }],
|
||||
]);
|
||||
|
||||
const KEY_COUNTS = {
|
||||
ONE: 90,
|
||||
LITE: 67,
|
||||
X: 256,
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
if (browser && navigator.serial === undefined && import.meta.env.TAURI_FAMILY !== undefined) {
|
||||
await import("./tauri-serial")
|
||||
if (
|
||||
browser &&
|
||||
navigator.serial === undefined &&
|
||||
import.meta.env.TAURI_FAMILY !== undefined
|
||||
) {
|
||||
await import("./tauri-serial");
|
||||
}
|
||||
|
||||
export async function getViablePorts(): Promise<SerialPort[]> {
|
||||
return navigator.serial.getPorts().then(ports =>
|
||||
ports.filter(it => {
|
||||
const {usbProductId, usbVendorId} = it.getInfo()
|
||||
return navigator.serial.getPorts().then((ports) =>
|
||||
ports.filter((it) => {
|
||||
const { usbProductId, usbVendorId } = it.getInfo();
|
||||
for (const filter of PORT_FILTERS.values()) {
|
||||
if (filter.usbProductId === usbProductId && filter.usbVendorId === usbVendorId) {
|
||||
return true
|
||||
if (
|
||||
filter.usbProductId === usbProductId &&
|
||||
filter.usbVendorId === usbVendorId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function canAutoConnect() {
|
||||
return getViablePorts().then(it => it.length === 1)
|
||||
return getViablePorts().then((it) => it.length === 1);
|
||||
}
|
||||
|
||||
function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
let timer: number
|
||||
let timer: number;
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error("Timeout")), ms) as unknown as number
|
||||
timer = setTimeout(
|
||||
() => reject(new Error("Timeout")),
|
||||
ms,
|
||||
) as unknown as number;
|
||||
}),
|
||||
]).finally(() => clearTimeout(timer))
|
||||
]).finally(() => clearTimeout(timer));
|
||||
}
|
||||
|
||||
export class CharaDevice {
|
||||
private port!: SerialPort
|
||||
private reader!: ReadableStreamDefaultReader<string>
|
||||
private port!: SerialPort;
|
||||
private reader!: ReadableStreamDefaultReader<string>;
|
||||
|
||||
private readonly abortController1 = new AbortController()
|
||||
private readonly abortController2 = new AbortController()
|
||||
private readonly abortController1 = new AbortController();
|
||||
private readonly abortController2 = new AbortController();
|
||||
|
||||
private streamClosed!: Promise<void>
|
||||
private streamClosed!: Promise<void>;
|
||||
|
||||
private lock?: Promise<true>
|
||||
private lock?: Promise<true>;
|
||||
|
||||
private readonly suspendDebounce = 100
|
||||
private suspendDebounceId?: number
|
||||
private readonly suspendDebounce = 100;
|
||||
private suspendDebounceId?: number;
|
||||
|
||||
version!: SemVer
|
||||
company!: "CHARACHORDER"
|
||||
device!: "ONE" | "LITE" | "X"
|
||||
chipset!: "M0" | "S2"
|
||||
keyCount!: 90 | 67 | 256
|
||||
version!: SemVer;
|
||||
company!: "CHARACHORDER";
|
||||
device!: "ONE" | "LITE" | "X";
|
||||
chipset!: "M0" | "S2";
|
||||
keyCount!: 90 | 67 | 256;
|
||||
|
||||
get portInfo() {
|
||||
return this.port.getInfo()
|
||||
return this.port.getInfo();
|
||||
}
|
||||
|
||||
constructor(private readonly baudRate = 115200) {}
|
||||
|
||||
async init(manual = false) {
|
||||
try {
|
||||
const ports = await getViablePorts()
|
||||
const ports = await getViablePorts();
|
||||
this.port =
|
||||
!manual && ports.length === 1
|
||||
? ports[0]
|
||||
: await navigator.serial.requestPort({filters: [...PORT_FILTERS.values()]})
|
||||
: await navigator.serial.requestPort({
|
||||
filters: [...PORT_FILTERS.values()],
|
||||
});
|
||||
|
||||
await this.port.open({baudRate: this.baudRate})
|
||||
const info = this.port.getInfo()
|
||||
serialLog.update(it => {
|
||||
await this.port.open({ baudRate: this.baudRate });
|
||||
const info = this.port.getInfo();
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "system",
|
||||
value: `Connected; ID: 0x${info.usbProductId?.toString(16)}; Vendor: 0x${info.usbVendorId?.toString(
|
||||
value: `Connected; ID: 0x${info.usbProductId?.toString(
|
||||
16,
|
||||
)}`,
|
||||
})
|
||||
return it
|
||||
})
|
||||
await this.port.close()
|
||||
)}; Vendor: 0x${info.usbVendorId?.toString(16)}`,
|
||||
});
|
||||
return it;
|
||||
});
|
||||
await this.port.close();
|
||||
|
||||
this.version = new SemVer(await this.send("VERSION").then(([version]) => version))
|
||||
const [company, device, chipset] = await this.send("ID")
|
||||
this.company = company as "CHARACHORDER"
|
||||
this.device = device as "ONE" | "LITE" | "X"
|
||||
this.chipset = chipset as "M0" | "S2"
|
||||
this.keyCount = KEY_COUNTS[this.device]
|
||||
this.version = new SemVer(
|
||||
await this.send("VERSION").then(([version]) => version),
|
||||
);
|
||||
const [company, device, chipset] = await this.send("ID");
|
||||
this.company = company as "CHARACHORDER";
|
||||
this.device = device as "ONE" | "LITE" | "X";
|
||||
this.chipset = chipset as "M0" | "S2";
|
||||
this.keyCount = KEY_COUNTS[this.device];
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
console.error(e)
|
||||
throw e
|
||||
alert(e);
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async suspend() {
|
||||
await this.reader.cancel()
|
||||
await this.reader.cancel();
|
||||
await this.streamClosed.catch(() => {
|
||||
/** noop */
|
||||
})
|
||||
this.reader.releaseLock()
|
||||
await this.port.close()
|
||||
serialLog.update(it => {
|
||||
});
|
||||
this.reader.releaseLock();
|
||||
await this.port.close();
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "system",
|
||||
value: "Connection suspended",
|
||||
})
|
||||
return it
|
||||
})
|
||||
});
|
||||
return it;
|
||||
});
|
||||
}
|
||||
|
||||
private async wake() {
|
||||
await this.port.open({baudRate: this.baudRate})
|
||||
const decoderStream = new TextDecoderStream()
|
||||
await this.port.open({ baudRate: this.baudRate });
|
||||
const decoderStream = new TextDecoderStream();
|
||||
this.streamClosed = this.port.readable!.pipeTo(decoderStream.writable, {
|
||||
signal: this.abortController1.signal,
|
||||
})
|
||||
});
|
||||
|
||||
this.reader = decoderStream
|
||||
.readable!.pipeThrough(new TransformStream(new LineBreakTransformer()), {
|
||||
signal: this.abortController2.signal,
|
||||
})
|
||||
.getReader()
|
||||
serialLog.update(it => {
|
||||
.getReader();
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "system",
|
||||
value: "Connection resumed",
|
||||
})
|
||||
return it
|
||||
})
|
||||
});
|
||||
return it;
|
||||
});
|
||||
}
|
||||
|
||||
private async internalRead() {
|
||||
try {
|
||||
const {value} = await timeout(this.reader.read(), 5000)
|
||||
serialLog.update(it => {
|
||||
const { value } = await timeout(this.reader.read(), 5000);
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "output",
|
||||
value: value!,
|
||||
})
|
||||
return it
|
||||
})
|
||||
return value!
|
||||
});
|
||||
return it;
|
||||
});
|
||||
return value!;
|
||||
} catch (e) {
|
||||
serialLog.update(it => {
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "output",
|
||||
value: `${e}`,
|
||||
})
|
||||
return it
|
||||
})
|
||||
});
|
||||
return it;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,61 +192,64 @@ export class CharaDevice {
|
||||
* Send a command to the device
|
||||
*/
|
||||
private async internalSend(...command: string[]) {
|
||||
const writer = this.port.writable!.getWriter()
|
||||
const writer = this.port.writable!.getWriter();
|
||||
try {
|
||||
serialLog.update(it => {
|
||||
serialLog.update((it) => {
|
||||
it.push({
|
||||
type: "input",
|
||||
value: command.join(" "),
|
||||
})
|
||||
return it
|
||||
})
|
||||
await writer.write(new TextEncoder().encode(`${command.join(" ")}\r\n`))
|
||||
});
|
||||
return it;
|
||||
});
|
||||
await writer.write(new TextEncoder().encode(`${command.join(" ")}\r\n`));
|
||||
} finally {
|
||||
writer.releaseLock()
|
||||
writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
async forget() {
|
||||
await this.port.forget()
|
||||
await this.port.forget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read/write to serial port
|
||||
*/
|
||||
async runWith<T>(
|
||||
callback: (send: typeof this.internalSend, read: typeof this.internalRead) => T | Promise<T>,
|
||||
callback: (
|
||||
send: typeof this.internalSend,
|
||||
read: typeof this.internalRead,
|
||||
) => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
while (this.lock) {
|
||||
await this.lock
|
||||
await this.lock;
|
||||
}
|
||||
const send = this.internalSend.bind(this)
|
||||
const read = this.internalRead.bind(this)
|
||||
let resolveLock: (result: true) => void
|
||||
this.lock = new Promise<true>(resolve => {
|
||||
resolveLock = resolve
|
||||
})
|
||||
let result!: T
|
||||
const send = this.internalSend.bind(this);
|
||||
const read = this.internalRead.bind(this);
|
||||
let resolveLock: (result: true) => void;
|
||||
this.lock = new Promise<true>((resolve) => {
|
||||
resolveLock = resolve;
|
||||
});
|
||||
let result!: T;
|
||||
try {
|
||||
if (this.suspendDebounceId) {
|
||||
clearTimeout(this.suspendDebounceId)
|
||||
clearTimeout(this.suspendDebounceId);
|
||||
} else {
|
||||
await this.wake()
|
||||
await this.wake();
|
||||
}
|
||||
result = await callback(send, read)
|
||||
result = await callback(send, read);
|
||||
} finally {
|
||||
delete this.lock
|
||||
delete this.lock;
|
||||
this.suspendDebounceId = setTimeout(() => {
|
||||
// cannot be locked here as all the code until clearTimeout is sync
|
||||
console.assert(this.lock === undefined)
|
||||
console.assert(this.lock === undefined);
|
||||
this.lock = this.suspend().then(() => {
|
||||
delete this.lock
|
||||
delete this.suspendDebounceId
|
||||
return true
|
||||
})
|
||||
}, this.suspendDebounce) as any
|
||||
resolveLock!(true)
|
||||
return result
|
||||
delete this.lock;
|
||||
delete this.suspendDebounceId;
|
||||
return true;
|
||||
});
|
||||
}, this.suspendDebounce) as any;
|
||||
resolveLock!(true);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,34 +258,40 @@ export class CharaDevice {
|
||||
*/
|
||||
async send(...command: string[]) {
|
||||
return this.runWith(async (send, read) => {
|
||||
await send(...command)
|
||||
const commandString = command.join(" ").replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
|
||||
return read().then(it => it.replace(new RegExp(`^${commandString} `), "").split(" "))
|
||||
})
|
||||
await send(...command);
|
||||
const commandString = command
|
||||
.join(" ")
|
||||
.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
||||
return read().then((it) =>
|
||||
it.replace(new RegExp(`^${commandString} `), "").split(" "),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async getChordCount(): Promise<number> {
|
||||
const [count] = await this.send("CML C0")
|
||||
return Number.parseInt(count)
|
||||
const [count] = await this.send("CML C0");
|
||||
return Number.parseInt(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a chord by index
|
||||
*/
|
||||
async getChord(index: number | number[]): Promise<Chord> {
|
||||
const [actions, phrase] = await this.send(`CML C1 ${index}`)
|
||||
const [actions, phrase] = await this.send(`CML C1 ${index}`);
|
||||
return {
|
||||
actions: parseChordActions(actions),
|
||||
phrase: parsePhrase(phrase),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the phrase for a set of actions
|
||||
*/
|
||||
async getChordPhrase(actions: number[]): Promise<number[] | undefined> {
|
||||
const [phrase] = await this.send(`CML C2 ${stringifyChordActions(actions)}`)
|
||||
return phrase === "2" ? undefined : parsePhrase(phrase)
|
||||
const [phrase] = await this.send(
|
||||
`CML C2 ${stringifyChordActions(actions)}`,
|
||||
);
|
||||
return phrase === "2" ? undefined : parsePhrase(phrase);
|
||||
}
|
||||
|
||||
async setChord(chord: Chord) {
|
||||
@@ -272,14 +300,17 @@ export class CharaDevice {
|
||||
"C3",
|
||||
stringifyChordActions(chord.actions),
|
||||
stringifyPhrase(chord.phrase),
|
||||
)
|
||||
if (status !== "0") console.error(`Failed with status ${status}`)
|
||||
);
|
||||
if (status !== "0") console.error(`Failed with status ${status}`);
|
||||
}
|
||||
|
||||
async deleteChord(chord: Pick<Chord, "actions">) {
|
||||
const status = await this.send(`CML C4 ${stringifyChordActions(chord.actions)}`)
|
||||
console.log(status)
|
||||
if (status.at(-1) !== "2" && status.at(-1) !== "0") throw new Error(`Failed with status ${status}`)
|
||||
const status = await this.send(
|
||||
`CML C4 ${stringifyChordActions(chord.actions)}`,
|
||||
);
|
||||
console.log(status);
|
||||
if (status.at(-1) !== "2" && status.at(-1) !== "0")
|
||||
throw new Error(`Failed with status ${status}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,9 +320,9 @@ export class CharaDevice {
|
||||
* @param action the assigned action id
|
||||
*/
|
||||
async setLayoutKey(layer: number, id: number, action: number) {
|
||||
const [status] = await this.send(`VAR B4 A${layer} ${id} ${action}`)
|
||||
console.log(status)
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||
const [status] = await this.send(`VAR B4 A${layer} ${id} ${action}`);
|
||||
console.log(status);
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,9 +332,9 @@ export class CharaDevice {
|
||||
* @returns the assigned action id
|
||||
*/
|
||||
async getLayoutKey(layer: number, id: number) {
|
||||
const [position, status] = await this.send(`VAR B3 A${layer} ${id}`)
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||
return Number(position)
|
||||
const [position, status] = await this.send(`VAR B3 A${layer} ${id}`);
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||
return Number(position);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,8 +345,8 @@ export class CharaDevice {
|
||||
* **This does not need to be called for chords**
|
||||
*/
|
||||
async commit() {
|
||||
const [status] = await this.send("VAR B0")
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||
const [status] = await this.send("VAR B0");
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,39 +356,47 @@ export class CharaDevice {
|
||||
* To permanently store the settings, you *must* call commit.
|
||||
*/
|
||||
async setSetting(id: number, value: number) {
|
||||
const [status] = await this.send(`VAR B2 ${id.toString(16).toUpperCase()} ${value}`)
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`)
|
||||
const [status] = await this.send(
|
||||
`VAR B2 ${id.toString(16).toUpperCase()} ${value}`,
|
||||
);
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a setting from the device
|
||||
*/
|
||||
async getSetting(id: number): Promise<number> {
|
||||
const [value, status] = await this.send(`VAR B1 ${id.toString(16).toUpperCase()}`)
|
||||
const [value, status] = await this.send(
|
||||
`VAR B1 ${id.toString(16).toUpperCase()}`,
|
||||
);
|
||||
if (status !== "0")
|
||||
throw new Error(`Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`)
|
||||
return Number(value)
|
||||
throw new Error(
|
||||
`Setting "0x${id.toString(16)}" doesn't exist (Status code ${status})`,
|
||||
);
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reboots the device
|
||||
*/
|
||||
async reboot() {
|
||||
await this.send("RST")
|
||||
await this.send("RST");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reboots the device to the bootloader
|
||||
*/
|
||||
async bootloader() {
|
||||
await this.send("RST BOOTLOADER")
|
||||
await this.send("RST BOOTLOADER");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the device
|
||||
*/
|
||||
async reset(type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC") {
|
||||
await this.send(`RST ${type}`)
|
||||
async reset(
|
||||
type: "FACTORY" | "PARAMS" | "KEYMAPS" | "STARTER" | "CLEARCML" | "FUNC",
|
||||
) {
|
||||
await this.send(`RST ${type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,6 +405,6 @@ export class CharaDevice {
|
||||
* This is useful for debugging when there is a suspected heap or stack issue.
|
||||
*/
|
||||
async getRamBytesAvailable(): Promise<number> {
|
||||
return Number(await this.send("RAM"))
|
||||
return Number(await this.send("RAM"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
import type {ActionInfo, KeymapCategory} from "$lib/assets/keymaps/keymap"
|
||||
import type { ActionInfo, KeymapCategory } from "$lib/assets/keymaps/keymap";
|
||||
|
||||
export interface KeyInfo extends Partial<ActionInfo> {
|
||||
code: number
|
||||
category: KeymapCategory
|
||||
code: number;
|
||||
category: KeymapCategory;
|
||||
}
|
||||
|
||||
export const KEYMAP_CATEGORIES = (await Promise.all(
|
||||
Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(async load =>
|
||||
load().then(it => (it as any).default),
|
||||
Object.values(import.meta.glob("$lib/assets/keymaps/*.yml")).map(
|
||||
async (load) => load().then((it) => (it as any).default),
|
||||
),
|
||||
)) as KeymapCategory[]
|
||||
)) as KeymapCategory[];
|
||||
|
||||
export const KEYMAP_CODES: Record<number, KeyInfo> = Object.fromEntries(
|
||||
KEYMAP_CATEGORIES.flatMap(category =>
|
||||
KEYMAP_CATEGORIES.flatMap((category) =>
|
||||
Object.entries(category.actions).map(([code, action]) => [
|
||||
Number(code),
|
||||
{...action, code: Number(code), category},
|
||||
{ ...action, code: Number(code), category },
|
||||
]),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
export const KEYMAP_KEYCODES: Map<string, number> = new Map(
|
||||
KEYMAP_CATEGORIES.flatMap(category =>
|
||||
Object.entries(category.actions).map(([code, action]) => [action.keyCode!, Number(code)] as const),
|
||||
KEYMAP_CATEGORIES.flatMap((category) =>
|
||||
Object.entries(category.actions).map(
|
||||
([code, action]) => [action.keyCode!, Number(code)] as const,
|
||||
),
|
||||
).filter(([keyCode]) => keyCode !== undefined),
|
||||
)
|
||||
);
|
||||
|
||||
export const KEYMAP_IDS: Map<string, KeyInfo> = new Map(
|
||||
KEYMAP_CATEGORIES.flatMap(category =>
|
||||
KEYMAP_CATEGORIES.flatMap((category) =>
|
||||
Object.entries(category.actions).map(
|
||||
([code, action]) => [action.id!, {...action, code: Number(code), category}] as const,
|
||||
([code, action]) =>
|
||||
[action.id!, { ...action, code: Number(code), category }] as const,
|
||||
),
|
||||
).filter(([id]) => id !== undefined),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
export class LineBreakTransformer {
|
||||
private chunks = ""
|
||||
private chunks = "";
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
transform(chunk: string, controller: TransformStreamDefaultController) {
|
||||
this.chunks += chunk
|
||||
const lines = this.chunks.split("\r\n")
|
||||
this.chunks = lines.pop()!
|
||||
this.chunks += chunk;
|
||||
const lines = this.chunks.split("\r\n");
|
||||
this.chunks = lines.pop()!;
|
||||
for (const line of lines) {
|
||||
controller.enqueue(line)
|
||||
controller.enqueue(line);
|
||||
}
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
flush(controller: TransformStreamDefaultController) {
|
||||
controller.enqueue(this.chunks)
|
||||
controller.enqueue(this.chunks);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -1,24 +1,24 @@
|
||||
export class SemVer {
|
||||
major = 0
|
||||
minor = 0
|
||||
patch = 0
|
||||
preRelease?: string
|
||||
meta?: string
|
||||
major = 0;
|
||||
minor = 0;
|
||||
patch = 0;
|
||||
preRelease?: string;
|
||||
meta?: string;
|
||||
|
||||
constructor(versionString: string) {
|
||||
const result =
|
||||
/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+))?$/.exec(
|
||||
versionString,
|
||||
)
|
||||
);
|
||||
if (!result) {
|
||||
console.error("Invalid version string:", versionString)
|
||||
console.error("Invalid version string:", versionString);
|
||||
} else {
|
||||
const [, major, minor, patch, preRelease, meta] = result
|
||||
this.major = Number.parseInt(major)
|
||||
this.minor = Number.parseInt(minor)
|
||||
this.patch = Number.parseInt(patch)
|
||||
if (preRelease) this.preRelease = preRelease
|
||||
if (meta) this.meta = meta
|
||||
const [, major, minor, patch, preRelease, meta] = result;
|
||||
this.major = Number.parseInt(major);
|
||||
this.minor = Number.parseInt(minor);
|
||||
this.patch = Number.parseInt(patch);
|
||||
if (preRelease) this.preRelease = preRelease;
|
||||
if (meta) this.meta = meta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ export class SemVer {
|
||||
`${this.major}.${this.minor}.${this.patch}` +
|
||||
(this.preRelease ? `-${this.preRelease}` : "") +
|
||||
(this.meta ? `+${this.meta}` : "")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,42 +2,53 @@
|
||||
* Compress JSON.stringify with gzip
|
||||
*/
|
||||
export async function stringifyCompressed<T>(chords: T): Promise<Blob> {
|
||||
const stream = new Blob([JSON.stringify(chords)]).stream().pipeThrough(new CompressionStream("gzip"))
|
||||
return await new Response(stream).blob()
|
||||
const stream = new Blob([JSON.stringify(chords)])
|
||||
.stream()
|
||||
.pipeThrough(new CompressionStream("gzip"));
|
||||
return await new Response(stream).blob();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress JSON.parse with gzip
|
||||
*/
|
||||
export async function parseCompressed<T>(blob: Blob): Promise<T> {
|
||||
const stream = blob.stream().pipeThrough(new DecompressionStream("deflate"))
|
||||
return await new Response(stream).json()
|
||||
const stream = blob.stream().pipeThrough(new DecompressionStream("deflate"));
|
||||
return await new Response(stream).json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Share JS object as url query param
|
||||
*/
|
||||
export async function getSharableUrl(name: string, data: any, baseHref = window.location.href): Promise<URL> {
|
||||
return new Promise(async resolve => {
|
||||
const reader = new FileReader()
|
||||
export async function getSharableUrl(
|
||||
name: string,
|
||||
data: any,
|
||||
baseHref = window.location.href,
|
||||
): Promise<URL> {
|
||||
return new Promise(async (resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
const base64String = (reader.result as string).replace(/^data:application\/octet-stream;base64,/, "")
|
||||
const url = new URL(baseHref)
|
||||
url.searchParams.set(name, base64String)
|
||||
resolve(url)
|
||||
}
|
||||
reader.readAsDataURL(await stringifyCompressed(data))
|
||||
})
|
||||
const base64String = (reader.result as string).replace(
|
||||
/^data:application\/octet-stream;base64,/,
|
||||
"",
|
||||
);
|
||||
const url = new URL(baseHref);
|
||||
url.searchParams.set(name, base64String);
|
||||
resolve(url);
|
||||
};
|
||||
reader.readAsDataURL(await stringifyCompressed(data));
|
||||
});
|
||||
}
|
||||
|
||||
export async function parseSharableUrl<T>(
|
||||
name: string,
|
||||
url: string = window.location.href,
|
||||
): Promise<T | undefined> {
|
||||
const searchParams = new URL(url).searchParams
|
||||
if (!searchParams.has(name)) return
|
||||
const searchParams = new URL(url).searchParams;
|
||||
if (!searchParams.has(name)) return;
|
||||
|
||||
return await fetch(`data:application/octet-stream;base64,${searchParams.get(name)}`)
|
||||
.then(it => it.blob())
|
||||
.then(it => parseCompressed(it))
|
||||
return await fetch(
|
||||
`data:application/octet-stream;base64,${searchParams.get(name)}`,
|
||||
)
|
||||
.then((it) => it.blob())
|
||||
.then((it) => parseCompressed(it));
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
/// <references types="@types/w3c-web-serial" />
|
||||
|
||||
interface SerialPortInfo {
|
||||
name?: string
|
||||
serialNumber?: string
|
||||
manufacturer?: string
|
||||
product?: string
|
||||
name?: string;
|
||||
serialNumber?: string;
|
||||
manufacturer?: string;
|
||||
product?: string;
|
||||
}
|
||||
|
||||
@@ -1,65 +1,77 @@
|
||||
import {invoke} from "@tauri-apps/api"
|
||||
import TauriSerialDialog from "$lib/serial/TauriSerialDialog.svelte"
|
||||
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
|
||||
return info;
|
||||
},
|
||||
async open({baudRate}: SerialOptions) {
|
||||
await invoke("plugin:serial|open", {path: info.name, baudRate})
|
||||
async open({ baudRate }: SerialOptions) {
|
||||
await invoke("plugin:serial|open", { path: info.name, baudRate });
|
||||
},
|
||||
async close() {
|
||||
await invoke("plugin:serial|close", {path: info.name})
|
||||
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))
|
||||
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)})
|
||||
await invoke("plugin:serial|write", {
|
||||
path: info.name,
|
||||
chunk: Array.from(chunk),
|
||||
});
|
||||
},
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error polyfill
|
||||
// noinspection JSConstantReassignment
|
||||
navigator.serial = {
|
||||
async getPorts(): Promise<SerialPort[]> {
|
||||
return invoke<any[]>("plugin:serial|get_serial_ports").then(ports =>
|
||||
return invoke<any[]>("plugin:serial|get_serial_ports").then((ports) =>
|
||||
ports.map(NativeSerialPort),
|
||||
) as Promise<SerialPort[]>
|
||||
) as Promise<SerialPort[]>;
|
||||
},
|
||||
async requestPort(options?: SerialPortRequestOptions): Promise<SerialPort> {
|
||||
const ports = await navigator.serial.getPorts().then(ports =>
|
||||
const ports = await navigator.serial.getPorts().then((ports) =>
|
||||
options?.filters !== undefined
|
||||
? ports.filter(port =>
|
||||
options.filters!.some(({usbVendorId, usbProductId}) => {
|
||||
const info = port.getInfo()
|
||||
? ports.filter((port) =>
|
||||
options.filters!.some(({ usbVendorId, usbProductId }) => {
|
||||
const info = port.getInfo();
|
||||
return (
|
||||
(usbVendorId === undefined || info.usbVendorId === usbVendorId) &&
|
||||
(usbProductId === undefined || info.usbProductId === usbProductId)
|
||||
)
|
||||
(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
|
||||
const dialog = new TauriSerialDialog({
|
||||
target: document.body,
|
||||
props: { ports },
|
||||
});
|
||||
const port = await new Promise<SerialPort>((resolve) =>
|
||||
dialog.$on("confirm", resolve),
|
||||
);
|
||||
dialog.$destroy();
|
||||
return port;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user