diff --git a/icons.config.js b/icons.config.js index 134098f6..b9a19daf 100644 --- a/icons.config.js +++ b/icons.config.js @@ -75,6 +75,7 @@ const config = { "light_mode", "palette", "translate", + "smart_toy", "play_arrow", "extension", "upload_file", diff --git a/src/lib/ccos/ccos-events.ts b/src/lib/ccos/ccos-events.ts index 7d2b56b3..872b259a 100644 --- a/src/lib/ccos/ccos-events.ts +++ b/src/lib/ccos/ccos-events.ts @@ -15,7 +15,7 @@ export interface CCOSKeyReleaseEvent { export interface CCOSSerialEvent { type: "serial"; - data: number; + data: Uint8Array; } export type CCOSInEvent = diff --git a/src/lib/ccos/ccos.ts b/src/lib/ccos/ccos.ts index a9d4aa5c..c9dce154 100644 --- a/src/lib/ccos/ccos.ts +++ b/src/lib/ccos/ccos.ts @@ -1,6 +1,7 @@ import { getMeta } from "$lib/meta/meta-storage"; -import { connectable, from, multicast, Subject } from "rxjs"; +import type { SerialPortLike } from "$lib/serial/device"; import type { + CCOSInEvent, CCOSInitEvent, CCOSKeyPressEvent, CCOSKeyReleaseEvent, @@ -8,7 +9,7 @@ import type { } from "./ccos-events"; import { KEYCODE_TO_SCANCODE, SCANCODE_TO_KEYCODE } from "./ccos-interop"; -const device = ".zero_wasm"; +const device = "zero_wasm"; class CCOSKeyboardEvent extends KeyboardEvent { constructor(...params: ConstructorParameters) { @@ -22,14 +23,17 @@ const MASK_ALT = 0b0100_0100; const MASK_ALT_GRAPH = 0b0000_0100; const MASK_GUI = 0b1000_1000; -export class CCOS { +export class CCOS implements SerialPortLike { private readonly currKeys = new Set(); private readonly layout = new Map(); private readonly worker = new Worker("/ccos-worker.js", { type: "module" }); - private ready = false; + private resolveReady!: () => void; + private ready = new Promise((resolve) => { + this.resolveReady = resolve; + }); private lastEvent?: KeyboardEvent; @@ -109,33 +113,29 @@ export class CCOS { this.currKeys.delete(0); } - private outStream = new Subject(); + private controller?: ReadableStreamDefaultController; - private readonly buffer: number[] = []; - private readonly outStream = new WritableStream({ - start(controller) {}, - }); - - readonly readable = connectable() - readonly writable = new WritableStream(); + readable!: ReadableStream; + writable!: WritableStream; constructor(url: string) { this.worker.addEventListener( "message", (event: MessageEvent) => { + if (event.data instanceof Uint8Array) { + this.controller?.enqueue(event.data); + return; + } + console.log("CCOS worker message", event.data); switch (event.data.type) { case "ready": { - this.ready = true; + this.resolveReady(); break; } case "report": { this.onReport(event.data.modifiers, event.data.keys); break; } - case "serial": { - this.outStream.next(event.data.data); - break; - } } }, ); @@ -152,7 +152,29 @@ export class CCOS { } satisfies CCOSInitEvent); } - async destroy() { + getInfo(): SerialPortInfo { + return {}; + } + + async open(_options: SerialOptions) { + this.readable = new ReadableStream({ + start: (controller) => { + this.controller = controller; + }, + }); + this.writable = new WritableStream({ + write: (chunk) => { + this.worker.postMessage(chunk, [chunk.buffer]); + }, + }); + return this.ready; + } + async close() { + await this.ready; + } + async forget() { + await this.ready; + this.close(); this.worker.terminate(); } @@ -198,7 +220,7 @@ export class CCOS { } export async function fetchCCOS( - version = ".test", + version = ".2.2.0-beta.12+266bdda", fetch: typeof window.fetch = window.fetch, ): Promise { const meta = await getMeta(device, version, fetch); diff --git a/src/lib/components/layout/Layout.svelte b/src/lib/components/layout/Layout.svelte index 4632f232..d1521908 100644 --- a/src/lib/components/layout/Layout.svelte +++ b/src/lib/components/layout/Layout.svelte @@ -24,6 +24,10 @@ import("$lib/assets/layouts/generic/103-key.yml").then( (it) => it.default as VisualLayout, ), + ZERO: () => + import("$lib/assets/layouts/generic/103-key.yml").then( + (it) => it.default as VisualLayout, + ), M4G: () => import("$lib/assets/layouts/m4g.yml").then( (it) => it.default as VisualLayout, diff --git a/src/lib/meta/meta-storage.ts b/src/lib/meta/meta-storage.ts index 77dcefe2..164a2d75 100644 --- a/src/lib/meta/meta-storage.ts +++ b/src/lib/meta/meta-storage.ts @@ -17,7 +17,7 @@ export async function getMeta( try { if (!browser) return fetchMeta(device, version, fetch); - const dbRequest = indexedDB.open("version-meta", 4); + const dbRequest = indexedDB.open("version-meta", 5); const db = await new Promise((resolve, reject) => { dbRequest.onsuccess = () => resolve(dbRequest.result); dbRequest.onerror = () => reject(dbRequest.error); @@ -144,6 +144,10 @@ async function fetchMeta( )?.name ?? undefined, esptool: meta?.update?.esptool ?? undefined, + js: meta?.update?.js ?? undefined, + wasm: meta?.update?.wasm ?? undefined, + dll: meta?.update?.dll ?? undefined, + so: meta?.update?.so ?? undefined, }, spiFlash: meta?.spi_flash ?? undefined, }; diff --git a/src/lib/meta/types/meta.ts b/src/lib/meta/types/meta.ts index a008aa0f..e5a3dbc2 100644 --- a/src/lib/meta/types/meta.ts +++ b/src/lib/meta/types/meta.ts @@ -52,6 +52,10 @@ export interface RawVersionMeta { ota: string | null; uf2: string | null; esptool: EspToolData | null; + js: string | null; + wasm: string | null; + dll: string | null; + so: string | null; }; files: string[]; spi_flash: SPIFlashInfo | null; @@ -78,6 +82,10 @@ export interface VersionMeta { ota?: string; uf2?: string; esptool?: EspToolData; + js?: string; + wasm?: string; + dll?: string; + so?: string; }; spiFlash?: SPIFlashInfo; } diff --git a/src/lib/serial/connection.ts b/src/lib/serial/connection.ts index 110ca29b..f0196aa1 100644 --- a/src/lib/serial/connection.ts +++ b/src/lib/serial/connection.ts @@ -1,5 +1,5 @@ import { get, writable } from "svelte/store"; -import { CharaDevice } from "$lib/serial/device"; +import { CharaDevice, type SerialPortLike } from "$lib/serial/device"; import type { Chord } from "$lib/serial/chord"; import type { Writable } from "svelte/store"; import type { CharaLayout } from "$lib/serialization/layout"; @@ -10,6 +10,10 @@ import type { VersionMeta } from "$lib/meta/types/meta"; export const serialPort = writable(); +navigator.serial?.addEventListener("disconnect", async (event) => { + serialPort.set(undefined); +}); + export interface SerialLogEntry { type: "input" | "output" | "system"; value: string; @@ -59,9 +63,13 @@ export interface ProgressInfo { } export const syncProgress = writable(undefined); -export async function initSerial(manual = false, withSync = true) { - const device = get(serialPort) ?? new CharaDevice(); - await device.init(manual); +export async function initSerial(port: SerialPortLike, withSync: boolean) { + const prev = get(serialPort); + try { + prev?.close(); + } catch {} + const device = new CharaDevice(port); + await device.init(); serialPort.set(device); if (withSync) { await sync(); diff --git a/src/lib/serial/device.ts b/src/lib/serial/device.ts index 90e7e5d2..25f4b657 100644 --- a/src/lib/serial/device.ts +++ b/src/lib/serial/device.ts @@ -11,7 +11,7 @@ import { browser } from "$app/environment"; import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog"; import semverGte from "semver/functions/gte"; -const PORT_FILTERS: Map = new Map([ +export const PORT_FILTERS: Map = new Map([ ["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }], ["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }], ["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }], @@ -23,6 +23,42 @@ const PORT_FILTERS: Map = new Map([ ["T4G S2", { usbProductId: 0x82f2, usbVendorId: 0x303a }], ]); +const DEVICE_ALIASES = new Map>([ + ["CC1", new Set(["ONE M0", "one_m0"])], + ["CC2", new Set(["TWO S3", "two_s3", "TWO S3 (pre-production)"])], + ["Lite (S2)", new Set(["LITE S2", "lite_s2"])], + ["Lite (M0)", new Set(["LITE M0", "lite_m0"])], + ["CCX", new Set(["X", "ccx"])], + ["M4G", new Set(["M4G S3", "m4g_s3", "M4G S3 (pre-production)"])], + ["M4G (right)", new Set(["M4GR S3", "m4gr_s3"])], + ["T4G", new Set(["T4G S2", "t4g_s2"])], +]); + +export function getName(alias: string): string { + for (const [name, aliases] of DEVICE_ALIASES.entries()) { + if (aliases.has(alias)) { + return name; + } + } + return alias; +} + +export function getPortName(port: SerialPort): string { + const { usbProductId, usbVendorId } = port.getInfo(); + console.log(port.getInfo()); + for (const [name, filter] of PORT_FILTERS.entries()) { + if ( + filter.usbProductId === usbProductId && + filter.usbVendorId === usbVendorId + ) { + return getName(name); + } + } + return `Unknown Device (0x${usbVendorId?.toString( + 16, + )}/0x${usbProductId?.toString(16)})`; +} + const KEY_COUNTS = { ONE: 90, TWO: 90, @@ -31,6 +67,7 @@ const KEY_COUNTS = { M4G: 90, M4GR: 90, T4G: 7, + ZERO: 256, } as const; if ( @@ -88,8 +125,12 @@ async function timeout(promise: Promise, ms: number): Promise { ]).finally(() => clearTimeout(timer)); } +export type SerialPortLike = Pick< + SerialPort, + "readable" | "writable" | "open" | "close" | "getInfo" | "forget" +>; + export class CharaDevice { - private port!: SerialPort; private reader!: ReadableStreamDefaultReader; private readonly abortController1 = new AbortController(); @@ -114,18 +155,13 @@ export class CharaDevice { return this.port.getInfo(); } - constructor(private readonly baudRate = 115200) {} + constructor( + private readonly port: SerialPortLike, + private readonly baudRate = 115200, + ) {} - async init(manual = false) { + async init() { try { - const ports = await getViablePorts(); - this.port = - !manual && ports.length === 1 - ? ports[0]! - : await navigator.serial.requestPort({ - filters: [...PORT_FILTERS.values()], - }); - await this.port.open({ baudRate: this.baudRate }); const info = this.port.getInfo(); serialLog.update((it) => { @@ -242,6 +278,10 @@ export class CharaDevice { await this.port.forget(); } + async close() { + await this.port.close(); + } + /** * Read/write to serial port */ diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 3e733de0..cd905f26 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -11,7 +11,7 @@ argbFromHex, themeFromSourceColor, } from "@material/material-color-utilities"; - import { canAutoConnect } from "$lib/serial/device"; + import { canAutoConnect, getViablePorts } from "$lib/serial/device"; import { initSerial } from "$lib/serial/connection"; import type { LayoutData } from "./$types"; import { browser } from "$app/environment"; @@ -63,7 +63,8 @@ } if (browser && $userPreferences.autoConnect && (await canAutoConnect())) { - await initSerial(); + const [port] = await getViablePorts(); + await initSerial(port!, true); } if (data.importFile) { diff --git a/src/routes/(app)/ConnectPopup.svelte b/src/routes/(app)/ConnectPopup.svelte new file mode 100644 index 00000000..8def3b51 --- /dev/null +++ b/src/routes/(app)/ConnectPopup.svelte @@ -0,0 +1,144 @@ + + +
+
+ + + +
+ + {#each ports as port} +
+ + +
+ {/each} +
+ + addVirtual Device +
+
+ + diff --git a/src/routes/(app)/Footer.svelte b/src/routes/(app)/Footer.svelte index 15f1993f..541d52c6 100644 --- a/src/routes/(app)/Footer.svelte +++ b/src/routes/(app)/Footer.svelte @@ -7,16 +7,14 @@ import { detectLocale, locales } from "$i18n/i18n-util"; import { loadLocaleAsync } from "$i18n/i18n-util.async"; import { tick } from "svelte"; - import SyncOverlay from "./SyncOverlay.svelte"; import { - initSerial, serialPort, sync, syncProgress, syncStatus, } from "$lib/serial/connection"; import { fade, slide } from "svelte/transition"; - import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog"; + import ConnectPopup from "./ConnectPopup.svelte"; let locale = $state( (browser && (localStorage.getItem("locale") as Locales)) || detectLocale(), @@ -48,20 +46,11 @@ } } - async function connect() { - try { - await initSerial(true); - } catch (error) { - console.error(error); - await showConnectionFailedDialog(String(error)); - } - } - function disconnect(event: MouseEvent) { if (event.shiftKey) { sync(); } else { - $serialPort?.forget(); + $serialPort?.close(); $serialPort = undefined; } } @@ -90,9 +79,15 @@
{#if !$serialPort} - +
+ +
{:else} + {/if} + {#if data.meta.update.ota && !data.meta.device.endsWith("m0")} {@const buttonError = error || (!success && isCorrectDevice === false)}
@@ -260,47 +265,49 @@
{/if} -
-
    -
  1. - - your device - {#if step >= 1} - check_circle - {/if} -
  2. + {#if data.meta.update.uf2} +
    +
      +
    1. + + your device + {#if step >= 1} + check_circle + {/if} +
    2. -
    3. - Make a - {#if step >= 2} - check_circle - {/if} -
    4. +
    5. + Make a + {#if step >= 2} + check_circle + {/if} +
    6. -
    7. - Reboot to - {#if step >= 3} - check_circle - {/if} -
    8. +
    9. + Reboot to + {#if step >= 3} + check_circle + {/if} +
    10. -
    11. - Replace - on the new drive - {#if step >= 4} - check_circle - {/if} -
    12. -
    -
    +
  3. + Replace + on the new drive + {#if step >= 4} + check_circle + {/if} +
  4. +
+
+ {/if} {#if false && data.meta.update.esptool}
diff --git a/src/routes/(app)/config/chords/+page.svelte b/src/routes/(app)/config/chords/+page.svelte index e2abc67a..30c4e77a 100644 --- a/src/routes/(app)/config/chords/+page.svelte +++ b/src/routes/(app)/config/chords/+page.svelte @@ -321,7 +321,7 @@ > {/if} - {#each $items.slice(page * $pageSize - (page === 0 ? 0 : 1), (page + 1) * $pageSize - 1) as [chord] (JSON.stringify(chord?.id))} + {#each $items.slice(page * $pageSize - (page === 0 ? 0 : 1), (page + 1) * $pageSize - 1) as [chord]} {#if chord} (page = 0)} /> {/if} diff --git a/static/ccos-worker.js b/static/ccos-worker.js index c52096ad..4e32809a 100644 --- a/static/ccos-worker.js +++ b/static/ccos-worker.js @@ -33,6 +33,7 @@ const ccosFsPath = "/CCOS"; /** @type {any} */ let ccos; +let startTime = 0; const semaphore = new AsyncSemaphore(); @@ -40,10 +41,17 @@ const semaphore = new AsyncSemaphore(); * @param {MessageEvent} event */ self.addEventListener("message", async (event) => { + if (event.data instanceof Uint8Array) { + await semaphore.run(() => serialWrite(event.data)); + return; + } switch (event.data.type) { case "init": { const url = event.data.url; await semaphore.run(() => init(url)); + /** @type {CCOSReadyEvent} */ + const readyMsg = { type: "ready" }; + self.postMessage(readyMsg); break; } case "press": { @@ -56,9 +64,6 @@ self.addEventListener("message", async (event) => { await semaphore.run(() => keyRelease(code)); break; } - case "serial": { - await semaphore.run(() => serialWrite(event.data.data)); - } } }); @@ -108,37 +113,44 @@ async function init(url) { * @param {number} data */ (data) => { - /** @type {CCOSInEvent}) */ - const msg = { type: "serial", data }; - self.postMessage(msg); + const array = new Uint8Array([data]); + self.postMessage(array, { transfer: [array.buffer] }); }, "vi", ); - ccos._init(onReport, onSerial); + startTime = performance.now(); + await ccos.ccall( + "init", + "void", + ["string", "number", "number"], + [ccosFsPath, onReport, onSerial], + { async: true }, + ); async function update() { if (ccos) { - await semaphore.run(() => ccos.update()); + await semaphore.run(() => { + ccos.update(performance.now()); + }); } requestAnimationFrame(update); } - update(); - /** @type {CCOSReadyEvent} */ - const readyMsg = { type: "ready" }; - self.postMessage(readyMsg); + requestAnimationFrame(update); } /** - * @param {number} data + * @param {Uint8Array} data */ async function serialWrite(data) { if (!ccos) { console.warn("Serial write ignored, CCOS is not initialized."); return; } - await ccos.serialWrite(data); + for (let i = 0; i < data.length; i++) { + await ccos.serialWrite(data[i]); + } } /**