feat: wasm zero

This commit is contained in:
2025-10-29 18:51:03 +01:00
parent 45682f0d1a
commit 1de52f7f81
14 changed files with 353 additions and 107 deletions

View File

@@ -75,6 +75,7 @@ const config = {
"light_mode", "light_mode",
"palette", "palette",
"translate", "translate",
"smart_toy",
"play_arrow", "play_arrow",
"extension", "extension",
"upload_file", "upload_file",

View File

@@ -15,7 +15,7 @@ export interface CCOSKeyReleaseEvent {
export interface CCOSSerialEvent { export interface CCOSSerialEvent {
type: "serial"; type: "serial";
data: number; data: Uint8Array;
} }
export type CCOSInEvent = export type CCOSInEvent =

View File

@@ -1,6 +1,7 @@
import { getMeta } from "$lib/meta/meta-storage"; import { getMeta } from "$lib/meta/meta-storage";
import { connectable, from, multicast, Subject } from "rxjs"; import type { SerialPortLike } from "$lib/serial/device";
import type { import type {
CCOSInEvent,
CCOSInitEvent, CCOSInitEvent,
CCOSKeyPressEvent, CCOSKeyPressEvent,
CCOSKeyReleaseEvent, CCOSKeyReleaseEvent,
@@ -8,7 +9,7 @@ import type {
} from "./ccos-events"; } from "./ccos-events";
import { KEYCODE_TO_SCANCODE, SCANCODE_TO_KEYCODE } from "./ccos-interop"; import { KEYCODE_TO_SCANCODE, SCANCODE_TO_KEYCODE } from "./ccos-interop";
const device = ".zero_wasm"; const device = "zero_wasm";
class CCOSKeyboardEvent extends KeyboardEvent { class CCOSKeyboardEvent extends KeyboardEvent {
constructor(...params: ConstructorParameters<typeof KeyboardEvent>) { constructor(...params: ConstructorParameters<typeof KeyboardEvent>) {
@@ -22,14 +23,17 @@ const MASK_ALT = 0b0100_0100;
const MASK_ALT_GRAPH = 0b0000_0100; const MASK_ALT_GRAPH = 0b0000_0100;
const MASK_GUI = 0b1000_1000; const MASK_GUI = 0b1000_1000;
export class CCOS { export class CCOS implements SerialPortLike {
private readonly currKeys = new Set<number>(); private readonly currKeys = new Set<number>();
private readonly layout = new Map<string, string>(); private readonly layout = new Map<string, string>();
private readonly worker = new Worker("/ccos-worker.js", { type: "module" }); private readonly worker = new Worker("/ccos-worker.js", { type: "module" });
private ready = false; private resolveReady!: () => void;
private ready = new Promise<void>((resolve) => {
this.resolveReady = resolve;
});
private lastEvent?: KeyboardEvent; private lastEvent?: KeyboardEvent;
@@ -109,33 +113,29 @@ export class CCOS {
this.currKeys.delete(0); this.currKeys.delete(0);
} }
private outStream = new Subject<number>(); private controller?: ReadableStreamDefaultController<Uint8Array>;
private readonly buffer: number[] = []; readable!: ReadableStream<Uint8Array>;
private readonly outStream = new WritableStream<number>({ writable!: WritableStream<Uint8Array>;
start(controller) {},
});
readonly readable = connectable()
readonly writable = new WritableStream<string>();
constructor(url: string) { constructor(url: string) {
this.worker.addEventListener( this.worker.addEventListener(
"message", "message",
(event: MessageEvent<CCOSOutEvent>) => { (event: MessageEvent<CCOSOutEvent>) => {
if (event.data instanceof Uint8Array) {
this.controller?.enqueue(event.data);
return;
}
console.log("CCOS worker message", event.data);
switch (event.data.type) { switch (event.data.type) {
case "ready": { case "ready": {
this.ready = true; this.resolveReady();
break; break;
} }
case "report": { case "report": {
this.onReport(event.data.modifiers, event.data.keys); this.onReport(event.data.modifiers, event.data.keys);
break; break;
} }
case "serial": {
this.outStream.next(event.data.data);
break;
}
} }
}, },
); );
@@ -152,7 +152,29 @@ export class CCOS {
} satisfies CCOSInitEvent); } satisfies CCOSInitEvent);
} }
async destroy() { getInfo(): SerialPortInfo {
return {};
}
async open(_options: SerialOptions) {
this.readable = new ReadableStream<Uint8Array>({
start: (controller) => {
this.controller = controller;
},
});
this.writable = new WritableStream<Uint8Array>({
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(); this.worker.terminate();
} }
@@ -198,7 +220,7 @@ export class CCOS {
} }
export async function fetchCCOS( export async function fetchCCOS(
version = ".test", version = ".2.2.0-beta.12+266bdda",
fetch: typeof window.fetch = window.fetch, fetch: typeof window.fetch = window.fetch,
): Promise<CCOS | undefined> { ): Promise<CCOS | undefined> {
const meta = await getMeta(device, version, fetch); const meta = await getMeta(device, version, fetch);

View File

@@ -24,6 +24,10 @@
import("$lib/assets/layouts/generic/103-key.yml").then( import("$lib/assets/layouts/generic/103-key.yml").then(
(it) => it.default as VisualLayout, (it) => it.default as VisualLayout,
), ),
ZERO: () =>
import("$lib/assets/layouts/generic/103-key.yml").then(
(it) => it.default as VisualLayout,
),
M4G: () => M4G: () =>
import("$lib/assets/layouts/m4g.yml").then( import("$lib/assets/layouts/m4g.yml").then(
(it) => it.default as VisualLayout, (it) => it.default as VisualLayout,

View File

@@ -17,7 +17,7 @@ export async function getMeta(
try { try {
if (!browser) return fetchMeta(device, version, fetch); 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<IDBDatabase>((resolve, reject) => { const db = await new Promise<IDBDatabase>((resolve, reject) => {
dbRequest.onsuccess = () => resolve(dbRequest.result); dbRequest.onsuccess = () => resolve(dbRequest.result);
dbRequest.onerror = () => reject(dbRequest.error); dbRequest.onerror = () => reject(dbRequest.error);
@@ -144,6 +144,10 @@ async function fetchMeta(
)?.name ?? )?.name ??
undefined, undefined,
esptool: meta?.update?.esptool ?? 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, spiFlash: meta?.spi_flash ?? undefined,
}; };

View File

@@ -52,6 +52,10 @@ export interface RawVersionMeta {
ota: string | null; ota: string | null;
uf2: string | null; uf2: string | null;
esptool: EspToolData | null; esptool: EspToolData | null;
js: string | null;
wasm: string | null;
dll: string | null;
so: string | null;
}; };
files: string[]; files: string[];
spi_flash: SPIFlashInfo | null; spi_flash: SPIFlashInfo | null;
@@ -78,6 +82,10 @@ export interface VersionMeta {
ota?: string; ota?: string;
uf2?: string; uf2?: string;
esptool?: EspToolData; esptool?: EspToolData;
js?: string;
wasm?: string;
dll?: string;
so?: string;
}; };
spiFlash?: SPIFlashInfo; spiFlash?: SPIFlashInfo;
} }

View File

@@ -1,5 +1,5 @@
import { get, writable } from "svelte/store"; 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 { Chord } from "$lib/serial/chord";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { CharaLayout } from "$lib/serialization/layout"; import type { CharaLayout } from "$lib/serialization/layout";
@@ -10,6 +10,10 @@ import type { VersionMeta } from "$lib/meta/types/meta";
export const serialPort = writable<CharaDevice | undefined>(); export const serialPort = writable<CharaDevice | undefined>();
navigator.serial?.addEventListener("disconnect", async (event) => {
serialPort.set(undefined);
});
export interface SerialLogEntry { export interface SerialLogEntry {
type: "input" | "output" | "system"; type: "input" | "output" | "system";
value: string; value: string;
@@ -59,9 +63,13 @@ export interface ProgressInfo {
} }
export const syncProgress = writable<ProgressInfo | undefined>(undefined); export const syncProgress = writable<ProgressInfo | undefined>(undefined);
export async function initSerial(manual = false, withSync = true) { export async function initSerial(port: SerialPortLike, withSync: boolean) {
const device = get(serialPort) ?? new CharaDevice(); const prev = get(serialPort);
await device.init(manual); try {
prev?.close();
} catch {}
const device = new CharaDevice(port);
await device.init();
serialPort.set(device); serialPort.set(device);
if (withSync) { if (withSync) {
await sync(); await sync();

View File

@@ -11,7 +11,7 @@ import { browser } from "$app/environment";
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog"; import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
import semverGte from "semver/functions/gte"; import semverGte from "semver/functions/gte";
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([ export const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }], ["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }], ["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }],
["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }], ["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }],
@@ -23,6 +23,42 @@ const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
["T4G S2", { usbProductId: 0x82f2, usbVendorId: 0x303a }], ["T4G S2", { usbProductId: 0x82f2, usbVendorId: 0x303a }],
]); ]);
const DEVICE_ALIASES = new Map<string, Set<string>>([
["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 = { const KEY_COUNTS = {
ONE: 90, ONE: 90,
TWO: 90, TWO: 90,
@@ -31,6 +67,7 @@ const KEY_COUNTS = {
M4G: 90, M4G: 90,
M4GR: 90, M4GR: 90,
T4G: 7, T4G: 7,
ZERO: 256,
} as const; } as const;
if ( if (
@@ -88,8 +125,12 @@ async function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
]).finally(() => clearTimeout(timer)); ]).finally(() => clearTimeout(timer));
} }
export type SerialPortLike = Pick<
SerialPort,
"readable" | "writable" | "open" | "close" | "getInfo" | "forget"
>;
export class CharaDevice { export class CharaDevice {
private port!: SerialPort;
private reader!: ReadableStreamDefaultReader<string>; private reader!: ReadableStreamDefaultReader<string>;
private readonly abortController1 = new AbortController(); private readonly abortController1 = new AbortController();
@@ -114,18 +155,13 @@ export class CharaDevice {
return this.port.getInfo(); 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 { 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 }); await this.port.open({ baudRate: this.baudRate });
const info = this.port.getInfo(); const info = this.port.getInfo();
serialLog.update((it) => { serialLog.update((it) => {
@@ -242,6 +278,10 @@ export class CharaDevice {
await this.port.forget(); await this.port.forget();
} }
async close() {
await this.port.close();
}
/** /**
* Read/write to serial port * Read/write to serial port
*/ */

View File

@@ -11,7 +11,7 @@
argbFromHex, argbFromHex,
themeFromSourceColor, themeFromSourceColor,
} from "@material/material-color-utilities"; } 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 { initSerial } from "$lib/serial/connection";
import type { LayoutData } from "./$types"; import type { LayoutData } from "./$types";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
@@ -63,7 +63,8 @@
} }
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) { if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
await initSerial(); const [port] = await getViablePorts();
await initSerial(port!, true);
} }
if (data.importFile) { if (data.importFile) {

View File

@@ -0,0 +1,144 @@
<script lang="ts">
import LL from "$i18n/i18n-svelte";
import { preference } from "$lib/preferences";
import { initSerial } from "$lib/serial/connection";
import {
getPortName,
PORT_FILTERS,
type SerialPortLike,
} from "$lib/serial/device";
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
import { onMount } from "svelte";
let ports = $state<SerialPort[]>([]);
onMount(() => {
refreshPorts();
});
async function refreshPorts() {
ports = await navigator.serial.getPorts();
}
async function connect(port: SerialPortLike, withSync: boolean) {
try {
await initSerial(port, withSync);
} catch (error) {
console.error(error);
await showConnectionFailedDialog(String(error));
}
}
</script>
<div class="device-list">
<fieldset>
<label
><input type="checkbox" use:preference={"autoConnect"} />
<div class="title">{$LL.deviceManager.AUTO_CONNECT()}</div>
</label>
<label
><input type="checkbox" use:preference={"backup"} />
<div class="title">{@html $LL.backup.AUTO_BACKUP()}</div>
</label>
</fieldset>
<button
onclick={async (event) => {
const { fetchCCOS } = await import("$lib/ccos/ccos");
const ccos = await fetchCCOS();
if (ccos) {
connect(ccos, !event.shiftKey);
}
}}
>
<span class="icon">history</span>
CC0</button
>
{#each ports as port}
<div class="device">
<button
onclick={(event) => {
connect(port, !event.shiftKey);
}}
>
<span class="icon">history</span>
{getPortName(port)}</button
>
<button
class="error"
onclick={() => {
port.forget();
refreshPorts();
}}><span class="icon">link_off</span></button
>
</div>
{/each}
<div class="pair">
<button
onclick={async (event) => {
const port = await navigator.serial.requestPort({
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
});
if (!port) return;
refreshPorts();
connect(port, true);
}}
class="primary"><span class="icon">add</span>Pair</button
>
<a href="/ccos/zero_wasm/"><span class="icon">add</span>Virtual Device</a>
</div>
</div>
<style lang="scss">
button,
a {
padding: 10px;
height: 32px;
font-size: 12px;
.icon {
font-size: 18px;
}
}
.pair {
display: flex;
}
.device {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
button {
flex: 1;
}
}
button.error {
color: var(--md-sys-color-error);
}
label {
display: flex;
position: relative;
flex-wrap: wrap;
justify-content: flex-start;
align-items: flex-start;
gap: 8px;
appearance: none;
padding: 0;
.title {
font-weight: 600;
}
}
fieldset {
display: flex;
gap: 16px;
border: none;
padding: 0;
}
</style>

View File

@@ -7,16 +7,14 @@
import { detectLocale, locales } from "$i18n/i18n-util"; import { detectLocale, locales } from "$i18n/i18n-util";
import { loadLocaleAsync } from "$i18n/i18n-util.async"; import { loadLocaleAsync } from "$i18n/i18n-util.async";
import { tick } from "svelte"; import { tick } from "svelte";
import SyncOverlay from "./SyncOverlay.svelte";
import { import {
initSerial,
serialPort, serialPort,
sync, sync,
syncProgress, syncProgress,
syncStatus, syncStatus,
} from "$lib/serial/connection"; } from "$lib/serial/connection";
import { fade, slide } from "svelte/transition"; import { fade, slide } from "svelte/transition";
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog"; import ConnectPopup from "./ConnectPopup.svelte";
let locale = $state( let locale = $state(
(browser && (localStorage.getItem("locale") as Locales)) || detectLocale(), (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) { function disconnect(event: MouseEvent) {
if (event.shiftKey) { if (event.shiftKey) {
sync(); sync();
} else { } else {
$serialPort?.forget(); $serialPort?.close();
$serialPort = undefined; $serialPort = undefined;
} }
} }
@@ -90,9 +79,15 @@
</ul> </ul>
<div class="sync-box"> <div class="sync-box">
{#if !$serialPort} {#if !$serialPort}
<button class="warning" onclick={connect} transition:slide={{ axis: "x" }} <button
class="warning"
popovertarget="connect-popup"
transition:slide={{ axis: "x" }}
><span class="icon">usb</span>{$LL.deviceManager.CONNECT()}</button ><span class="icon">usb</span>{$LL.deviceManager.CONNECT()}</button
> >
<div popover id="connect-popup">
<ConnectPopup />
</div>
{:else} {:else}
<button <button
transition:slide={{ axis: "x" }} transition:slide={{ axis: "x" }}

View File

@@ -72,7 +72,8 @@
async function connect() { async function connect() {
try { try {
await initSerial(true, false); const port = await navigator.serial.requestPort();
await initSerial(port!, true);
step = 1; step = 1;
} catch (e) { } catch (e) {
error = e as Error; error = e as Error;
@@ -197,6 +198,10 @@
</script> </script>
<div class="container"> <div class="container">
{#if data.meta.update.js && data.meta.update.wasm}
<button>Add Virtual Device</button>
{/if}
{#if data.meta.update.ota && !data.meta.device.endsWith("m0")} {#if data.meta.update.ota && !data.meta.device.endsWith("m0")}
{@const buttonError = error || (!success && isCorrectDevice === false)} {@const buttonError = error || (!success && isCorrectDevice === false)}
<section> <section>
@@ -260,47 +265,49 @@
</div> </div>
{/if} {/if}
<section> {#if data.meta.update.uf2}
<ol> <section>
<li> <ol>
<button class="inline-button" onclick={connect} <li>
><span class="icon">usb</span>Connect</button <button class="inline-button" onclick={connect}
> ><span class="icon">usb</span>Connect</button
your device >
{#if step >= 1} your device
<span class="icon ok" transition:fade>check_circle</span> {#if step >= 1}
{/if} <span class="icon ok" transition:fade>check_circle</span>
</li> {/if}
</li>
<li class:faded={step < 1}> <li class:faded={step < 1}>
Make a <button class="inline-button" onclick={backup} Make a <button class="inline-button" onclick={backup}
><span class="icon">download</span>Backup</button ><span class="icon">download</span>Backup</button
> >
{#if step >= 2} {#if step >= 2}
<span class="icon ok" transition:fade>check_circle</span> <span class="icon ok" transition:fade>check_circle</span>
{/if} {/if}
</li> </li>
<li class:faded={step < 2}> <li class:faded={step < 2}>
Reboot to <button class="inline-button" onclick={bootloader} Reboot to <button class="inline-button" onclick={bootloader}
><span class="icon">restart_alt</span>Bootloader</button ><span class="icon">restart_alt</span>Bootloader</button
> >
{#if step >= 3} {#if step >= 3}
<span class="icon ok" transition:fade>check_circle</span> <span class="icon ok" transition:fade>check_circle</span>
{/if} {/if}
</li> </li>
<li class:faded={step < 3}> <li class:faded={step < 3}>
Replace <button class="inline-button" onclick={getFileSystem} Replace <button class="inline-button" onclick={getFileSystem}
><span class="icon">deployed_code_update</span>CURRENT.UF2</button ><span class="icon">deployed_code_update</span>CURRENT.UF2</button
> >
on the new drive on the new drive
{#if step >= 4} {#if step >= 4}
<span class="icon ok" transition:fade>check_circle</span> <span class="icon ok" transition:fade>check_circle</span>
{/if} {/if}
</li> </li>
</ol> </ol>
</section> </section>
{/if}
{#if false && data.meta.update.esptool} {#if false && data.meta.update.esptool}
<section> <section>

View File

@@ -321,7 +321,7 @@
><td></td><td></td></tr ><td></td><td></td></tr
> >
{/if} {/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} {#if chord}
<ChordEdit {chord} onduplicate={() => (page = 0)} /> <ChordEdit {chord} onduplicate={() => (page = 0)} />
{/if} {/if}

View File

@@ -33,6 +33,7 @@ const ccosFsPath = "/CCOS";
/** @type {any} */ /** @type {any} */
let ccos; let ccos;
let startTime = 0;
const semaphore = new AsyncSemaphore(); const semaphore = new AsyncSemaphore();
@@ -40,10 +41,17 @@ const semaphore = new AsyncSemaphore();
* @param {MessageEvent<CCOSInEvent>} event * @param {MessageEvent<CCOSInEvent>} event
*/ */
self.addEventListener("message", async (event) => { self.addEventListener("message", async (event) => {
if (event.data instanceof Uint8Array) {
await semaphore.run(() => serialWrite(event.data));
return;
}
switch (event.data.type) { switch (event.data.type) {
case "init": { case "init": {
const url = event.data.url; const url = event.data.url;
await semaphore.run(() => init(url)); await semaphore.run(() => init(url));
/** @type {CCOSReadyEvent} */
const readyMsg = { type: "ready" };
self.postMessage(readyMsg);
break; break;
} }
case "press": { case "press": {
@@ -56,9 +64,6 @@ self.addEventListener("message", async (event) => {
await semaphore.run(() => keyRelease(code)); await semaphore.run(() => keyRelease(code));
break; break;
} }
case "serial": {
await semaphore.run(() => serialWrite(event.data.data));
}
} }
}); });
@@ -108,37 +113,44 @@ async function init(url) {
* @param {number} data * @param {number} data
*/ */
(data) => { (data) => {
/** @type {CCOSInEvent}) */ const array = new Uint8Array([data]);
const msg = { type: "serial", data }; self.postMessage(array, { transfer: [array.buffer] });
self.postMessage(msg);
}, },
"vi", "vi",
); );
ccos._init(onReport, onSerial); startTime = performance.now();
await ccos.ccall(
"init",
"void",
["string", "number", "number"],
[ccosFsPath, onReport, onSerial],
{ async: true },
);
async function update() { async function update() {
if (ccos) { if (ccos) {
await semaphore.run(() => ccos.update()); await semaphore.run(() => {
ccos.update(performance.now());
});
} }
requestAnimationFrame(update); requestAnimationFrame(update);
} }
update();
/** @type {CCOSReadyEvent} */ requestAnimationFrame(update);
const readyMsg = { type: "ready" };
self.postMessage(readyMsg);
} }
/** /**
* @param {number} data * @param {Uint8Array} data
*/ */
async function serialWrite(data) { async function serialWrite(data) {
if (!ccos) { if (!ccos) {
console.warn("Serial write ignored, CCOS is not initialized."); console.warn("Serial write ignored, CCOS is not initialized.");
return; return;
} }
await ccos.serialWrite(data); for (let i = 0; i < data.length; i++) {
await ccos.serialWrite(data[i]);
}
} }
/** /**