mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-21 09:23:00 +00:00
refactor: use standard prettier formatting
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user