diff --git a/src/lib/serial/chord.spec.ts b/src/lib/serial/chord.spec.ts index 59d5eb88..2f3b7ba3 100644 --- a/src/lib/serial/chord.spec.ts +++ b/src/lib/serial/chord.spec.ts @@ -1,9 +1,11 @@ import {describe, it, expect} from "vitest" import { - chordAsCommandCompatible, - chordFromCommandCompatible, deserializeActions, + parseChordActions, + parsePhrase, serializeActions, + stringifyChordActions, + stringifyPhrase, } from "./chord" describe("chords", function () { @@ -24,18 +26,23 @@ describe("chords", function () { } }) - describe("commands", function () { - it("should convert to a command", function () { - expect(chordAsCommandCompatible({actions: [32, 51], phrase: [0x01, 0x68, 0x72, 0xd4, 0x65]})).toEqual( - "000CC200000000000000000000000000 016872D465", - ) + describe("phrase", function () { + it("should stringify", function () { + expect(stringifyPhrase([0x01, 0x68, 0x72, 0xd4, 0x65])).toEqual("016872D465") }) - it("should parse a command", function () { - expect(chordFromCommandCompatible("000CC200000000000000000000000000 016872D465")).toEqual({ - actions: [32, 51], - phrase: [0x01, 0x68, 0x72, 0xd4, 0x65], - }) + it("should parse", function () { + expect(parsePhrase("016872D465")).toEqual([0x01, 0x68, 0x72, 0xd4, 0x65]) + }) + }) + + describe("chord actions", function () { + it("should stringify", function () { + expect(stringifyChordActions([32, 51])).toEqual("000CC200000000000000000000000000") + }) + + it("should parse", function () { + expect(parseChordActions("000CC200000000000000000000000000")).toEqual([32, 51]) }) }) }) diff --git a/src/lib/serial/chord.ts b/src/lib/serial/chord.ts index 2da7dd16..b3241950 100644 --- a/src/lib/serial/chord.ts +++ b/src/lib/serial/chord.ts @@ -3,30 +3,25 @@ export interface Chord { phrase: number[] } -/** - * Turns a chord into a serial-command-compatible string - * - * @example "000CC200000000000000000000000000 7468726565" - */ -export function chordAsCommandCompatible(chord: Chord): string { - return `${serializeActions(chord.actions).toString(16).padStart(32, "0")} ${chord.phrase - .map(it => it.toString(16).padStart(2, "0")) - .join("")}`.toUpperCase() +export function parsePhrase(phrase: string): number[] { + return Array.from({length: phrase.length / 2}).map((_, i) => + Number.parseInt(phrase.slice(i * 2, i * 2 + 2), 16), + ) } -/** - * Turns a command response into a chord - * - * @see {chordAsCommandCompatible} - */ -export function chordFromCommandCompatible(command: string): Chord { - const [actions, phrase] = command.split(" ") - return { - actions: deserializeActions(BigInt(`0x${actions}`)), - phrase: Array.from({length: phrase.length / 2}).map((_, i) => - Number.parseInt(phrase.slice(i * 2, i * 2 + 2), 16), - ), - } +export function stringifyPhrase(phrase: number[]): string { + return phrase + .map(it => it.toString(16).padStart(2, "0")) + .join("") + .toUpperCase() +} + +export function parseChordActions(actions: string): number[] { + return deserializeActions(BigInt(`0x${actions}`)) +} + +export function stringifyChordActions(actions: number[]): string { + return serializeActions(actions).toString(16).padStart(32, "0").toUpperCase() } /** diff --git a/src/lib/serial/connection.ts b/src/lib/serial/connection.ts index 575875d2..3612a0a0 100644 --- a/src/lib/serial/connection.ts +++ b/src/lib/serial/connection.ts @@ -17,14 +17,14 @@ export const chords = writable([]) export const layout = writable([[], [], []]) +export const settings = writable({}) + export const unsavedChanges = writable(0) export const highlightActions: Writable = writable([]) export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"> = writable("done") -let device: CharaDevice // @hmr:keep - export async function initSerial(manual = false) { const device = get(serialPort) ?? new CharaDevice() await device.init(manual) diff --git a/src/lib/serial/device.ts b/src/lib/serial/device.ts index 3efb8a80..102729b8 100644 --- a/src/lib/serial/device.ts +++ b/src/lib/serial/device.ts @@ -1,7 +1,7 @@ import {LineBreakTransformer} from "$lib/serial/line-break-transformer" import {serialLog} from "$lib/serial/connection" import type {Chord} from "$lib/serial/chord" -import {chordFromCommandCompatible} from "$lib/serial/chord" +import {parseChordActions, parsePhrase, stringifyChordActions, stringifyPhrase} from "$lib/serial/chord" export const VENDOR_ID = 0x239a @@ -58,8 +58,8 @@ export class CharaDevice { }) .getReader() - this.version = await this.send("VERSION") - this.deviceId = await this.send("ID") + this.version = await this.send("VERSION").then(it => it[0]) + this.deviceId = await this.send("ID").then(it => it[0]) } private async internalRead() { @@ -138,21 +138,128 @@ export class CharaDevice { 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} `), "")) + return read().then(it => it.replace(new RegExp(`^${commandString} `), "").split(" ")) }) } async getChordCount(): Promise { - return Number.parseInt(await this.send("CML C0")) + const [count] = await this.send("CML C0") + return Number.parseInt(count) } - async getChord(index: number): Promise { - return chordFromCommandCompatible(await this.send(`CML C1 ${index}`)) + /** + * Retrieves a chord by index + */ + async getChord(index: number | number[]): Promise { + 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 { + const [phrase] = await this.send(`CML C2 ${stringifyChordActions(actions)}`) + return phrase === "0" ? undefined : parsePhrase(phrase) + } + + async setChord(chord: Chord) { + const [status] = await this.send( + "CML", + "C3", + stringifyChordActions(chord.actions), + stringifyPhrase(chord.phrase), + ) + if (status !== "0") throw new Error(`Failed with status ${status}`) + } + + async deleteChord(chord: Chord) { + const status = await this.send(`CML C4 ${stringifyChordActions(chord.actions)}`) + if (status.at(-1) !== "0") throw new Error(`Failed with status ${status}`) + } + + /** + * Sets an action to the layout + * @param layer the layer (usually 1-3) + * @param id id of the key, refer to the individual device for where each key is + * @param action the assigned action id + */ + async setLayoutKey(layer: number, id: number, action: number) { + const [status] = await this.send(`VAR B3 A${layer} ${id} ${action}`) + if (status !== "0") throw new Error(`Failed with status ${status}`) + } + + /** + * Gets the assigned action from the layout + * @param layer the layer (usually 1-3) + * @param id id of the key, refer to the individual device for where each key is + * @returns the assigned action id + */ async getLayoutKey(layer: number, id: number) { - const layout = await this.send(`VAR B3 A${layer} ${id}`) - const [position] = layout.split(" ").map(Number) - return 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) + } + + /** + * Permanently stores settings and layout to the device. + * + * CAUTION: Device may degrade prematurely above 10,000-25,000 commits. + * + * **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}`) + } + + /** + * Sets a setting on the device. + * + * Settings are applied until the next reboot or loss of power. + * 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).padStart(2, "0").toUpperCase()} ${value}`) + if (status !== "0") throw new Error(`Failed with status ${status}`) + } + + /** + * Retrieves a setting from the device + */ + async getSetting(id: number): Promise { + const [value, status] = await this.send(`VAR B1 ${id.toString(16).padStart(2, "0").toUpperCase()}`) + if (status !== "0") throw new Error(`Setting "${id}" doesn't exist (Status code ${status})`) + return Number(value) + } + + /** + * Reboots the device + */ + async reboot() { + await this.send("RST") + await this.disconnect() + // TODO: reconnect + } + + /** + * Reboots the device to the bootloader + */ + async bootloader() { + await this.send("RST BOOTLOADER") + await this.disconnect() + // TODO: more... + } + + /** + * Returns the current number of bytes available in SRAM. + * + * This is useful for debugging when there is a suspected heap or stack issue. + */ + async getRamBytesAvailable(): Promise { + return Number(await this.send("RAM")) } } diff --git a/src/lib/serial/settings.ts b/src/lib/serial/settings.ts new file mode 100644 index 00000000..73c5e172 --- /dev/null +++ b/src/lib/serial/settings.ts @@ -0,0 +1,41 @@ +export interface DeviceSettings { + enableSerialLog: boolean + enableSerialRaw: boolean + enableSerialChord: boolean + enableSerialKeyboard: boolean + enableSerialMouse: boolean + enableSerialDebug: boolean + enableSerialHeader: boolean + enableHidKeyboard: boolean + pressThreshold: number + releaseThreshold: number + enableHidMouse: number + scrollDelay: number + enableSpurring: boolean + spurKillerToggle: number + spurKiller: number + enableChording: boolean + charKillerToggle: number + charCounterKiller: number +} + +export const SETTING_IDS: Record = { + enableSerialLog: 0x01, + enableSerialRaw: 0x02, + enableSerialChord: 0x03, + enableSerialKeyboard: 0x04, + enableSerialMouse: 0x05, + enableSerialDebug: 0x06, + enableSerialHeader: 0x07, + enableHidKeyboard: 0x0a, + pressThreshold: 0x0b, + releaseThreshold: 0x0c, + enableHidMouse: 0x14, + scrollDelay: 0x15, + enableSpurring: 0x1e, + spurKillerToggle: 0x1f, + spurKiller: 0x20, + enableChording: 0x28, + charKillerToggle: 0x29, + charCounterKiller: 0x2a, +} diff --git a/src/routes/ConnectionPopup.svelte b/src/routes/ConnectionPopup.svelte index 2d2460d2..fb15cba3 100644 --- a/src/routes/ConnectionPopup.svelte +++ b/src/routes/ConnectionPopup.svelte @@ -56,8 +56,18 @@
(powerDialog = !powerDialog)} />

Boot Menu

- - + +
{/if} {/if} diff --git a/src/routes/config/chords/+page.svelte b/src/routes/config/chords/+page.svelte index 68155395..4addd3d9 100644 --- a/src/routes/config/chords/+page.svelte +++ b/src/routes/config/chords/+page.svelte @@ -1,5 +1,5 @@ + +