mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-31 06:12:39 +00:00
feat: ccos emulator
This commit is contained in:
@@ -57,6 +57,7 @@ const config = {
|
||||
"graphic_eq",
|
||||
"mail",
|
||||
"calculate",
|
||||
"playground_2",
|
||||
"open_in_browser",
|
||||
"chevron_backward",
|
||||
"chevron_forward",
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import type { Attachment } from "svelte/attachments";
|
||||
import { browser } from "$app/environment";
|
||||
import { persistentWritable } from "$lib/storage";
|
||||
import type { CharaDevice } from "$lib/serial/device";
|
||||
import type { CCOS, CCOSKeyboardEvent } from "./ccos";
|
||||
import type { ReplayRecorder } from "$lib/charrecorder/core/recorder";
|
||||
|
||||
export const emulatedCCOS = persistentWritable("emulatedCCOS", false);
|
||||
|
||||
export function ccosKeyInterceptor() {
|
||||
return ((element: Window) => {
|
||||
const ccos = browser
|
||||
? import("./ccos").then((module) => module.fetchCCOS(".test"))
|
||||
: Promise.resolve(undefined);
|
||||
export function ccosKeyInterceptor(
|
||||
port: CharaDevice | undefined,
|
||||
recorder: ReplayRecorder,
|
||||
) {
|
||||
return ((element: HTMLElement) => {
|
||||
const ccos =
|
||||
port?.port && "handleKeyEvent" in port?.port
|
||||
? (port.port as CCOS)
|
||||
: undefined;
|
||||
console.log("Attaching CCOS key interceptor", ccos);
|
||||
|
||||
function onEvent(event: KeyboardEvent) {
|
||||
ccos.then((it) => it?.handleKeyEvent(event));
|
||||
ccos?.handleKeyEvent(event);
|
||||
if (!event.defaultPrevented) {
|
||||
recorder.next(event);
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener("keydown", onEvent, true);
|
||||
element.addEventListener("keyup", onEvent, true);
|
||||
if (ccos) {
|
||||
element.addEventListener("keydown", onEvent, true);
|
||||
element.addEventListener("keyup", onEvent, true);
|
||||
element.add;
|
||||
}
|
||||
|
||||
return () => {
|
||||
ccos.then((it) => it?.destroy());
|
||||
element.removeEventListener("keydown", onEvent, true);
|
||||
element.removeEventListener("keyup", onEvent, true);
|
||||
};
|
||||
}) satisfies Attachment<Window>;
|
||||
}) satisfies Attachment<HTMLElement>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getMeta } from "$lib/meta/meta-storage";
|
||||
import type { SerialPortLike } from "$lib/serial/device";
|
||||
import type {
|
||||
CCOSInEvent,
|
||||
CCOSInitEvent,
|
||||
CCOSKeyPressEvent,
|
||||
CCOSKeyReleaseEvent,
|
||||
@@ -11,7 +10,7 @@ import { KEYCODE_TO_SCANCODE, SCANCODE_TO_KEYCODE } from "./ccos-interop";
|
||||
|
||||
const device = "zero_wasm";
|
||||
|
||||
class CCOSKeyboardEvent extends KeyboardEvent {
|
||||
export class CCOSKeyboardEvent extends KeyboardEvent {
|
||||
constructor(...params: ConstructorParameters<typeof KeyboardEvent>) {
|
||||
super(...params);
|
||||
}
|
||||
@@ -26,7 +25,46 @@ const MASK_GUI = 0b1000_1000;
|
||||
export class CCOS implements SerialPortLike {
|
||||
private readonly currKeys = new Set<number>();
|
||||
|
||||
private readonly layout = new Map<string, string>();
|
||||
private readonly layout = new Map<string, string>([
|
||||
...Array.from(
|
||||
{ length: 26 },
|
||||
(_, i) =>
|
||||
[
|
||||
JSON.stringify([`Key${String.fromCharCode(65 + i)}`, "Shift"]),
|
||||
String.fromCharCode(65 + i),
|
||||
] as const,
|
||||
),
|
||||
...Array.from(
|
||||
{ length: 10 },
|
||||
(_, i) => [JSON.stringify([`Key${i}`]), i.toString()] as const,
|
||||
),
|
||||
|
||||
[JSON.stringify(["Space"]), " "],
|
||||
[JSON.stringify(["Backquote"]), "`"],
|
||||
[JSON.stringify(["Minus"]), "-"],
|
||||
[JSON.stringify(["Comma"]), ","],
|
||||
[JSON.stringify(["Period"]), "."],
|
||||
[JSON.stringify(["Semicolon"]), ";"],
|
||||
[JSON.stringify(["Equal"]), "="],
|
||||
|
||||
[JSON.stringify(["Backquote", "Shift"]), "~"],
|
||||
[JSON.stringify(["Minus", "Shift"]), "_"],
|
||||
[JSON.stringify(["Comma", "Shift"]), "<"],
|
||||
[JSON.stringify(["Period", "Shift"]), ">"],
|
||||
[JSON.stringify(["Semicolon", "Shift"]), ":"],
|
||||
[JSON.stringify(["Equal", "Shift"]), "+"],
|
||||
|
||||
[JSON.stringify(["Digit0", "Shift"]), ")"],
|
||||
[JSON.stringify(["Digit1", "Shift"]), "!"],
|
||||
[JSON.stringify(["Digit2", "Shift"]), "@"],
|
||||
[JSON.stringify(["Digit3", "Shift"]), "#"],
|
||||
[JSON.stringify(["Digit4", "Shift"]), "$"],
|
||||
[JSON.stringify(["Digit5", "Shift"]), "%"],
|
||||
[JSON.stringify(["Digit6", "Shift"]), "^"],
|
||||
[JSON.stringify(["Digit7", "Shift"]), "&"],
|
||||
[JSON.stringify(["Digit8", "Shift"]), "*"],
|
||||
[JSON.stringify(["Digit9", "Shift"]), "("],
|
||||
]);
|
||||
|
||||
private readonly worker = new Worker("/ccos-worker.js", { type: "module" });
|
||||
|
||||
@@ -126,7 +164,6 @@ export class CCOS implements SerialPortLike {
|
||||
this.controller?.enqueue(event.data);
|
||||
return;
|
||||
}
|
||||
console.log("CCOS worker message", event.data);
|
||||
switch (event.data.type) {
|
||||
case "ready": {
|
||||
this.resolveReady();
|
||||
@@ -220,7 +257,7 @@ export class CCOS implements SerialPortLike {
|
||||
}
|
||||
|
||||
export async function fetchCCOS(
|
||||
version = ".2.2.0-beta.12+266bdda",
|
||||
version = "3.0.0-rc.0",
|
||||
fetch: typeof window.fetch = window.fetch,
|
||||
): Promise<CCOS | undefined> {
|
||||
const meta = await getMeta(device, version, fetch);
|
||||
|
||||
@@ -147,7 +147,7 @@ export class CharaDevice {
|
||||
version!: string;
|
||||
company!: "CHARACHORDER" | "FORGE";
|
||||
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G" | "ENGINE" | "ZERO";
|
||||
chipset!: "M0" | "S2" | "S3";
|
||||
chipset!: "M0" | "S2" | "S3" | "WASM";
|
||||
keyCount!: 90 | 67 | 256;
|
||||
layerCount = 3;
|
||||
profileCount = 1;
|
||||
@@ -157,7 +157,7 @@ export class CharaDevice {
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly port: SerialPortLike,
|
||||
readonly port: SerialPortLike,
|
||||
public baudRate = 115200,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -46,15 +46,6 @@
|
||||
element?.closest<HTMLElement>("[popover]")?.hidePopover();
|
||||
}
|
||||
|
||||
async function connectCC0(event: MouseEvent) {
|
||||
const { fetchCCOS } = await import("$lib/ccos/ccos");
|
||||
closePopover();
|
||||
const ccos = await fetchCCOS();
|
||||
if (ccos) {
|
||||
connect(ccos, !event.shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
async function connectDevice(event: MouseEvent) {
|
||||
const port = await navigator.serial.requestPort({
|
||||
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
|
||||
|
||||
@@ -116,6 +116,25 @@
|
||||
{/if}
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href={import.meta.env.VITE_DISCORD_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
class="discord-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 126.64 96"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m81 0-3 7Q63 4 49 7l-4-7-26 8Q-4 45 1 80q14 10 32 16l6-11-10-5 2-2q33 13 64 0l3 2-11 5 7 11q17-5 32-16 4-40-19-72-12-5-26-8M42 65q-10-1-11-12 0-15 11-13c11 2 12 6 12 13q-1 11-12 12m42 0q-10-1-11-12 0-15 11-13c11 2 12 6 12 13q-1 11-12 12"
|
||||
/></svg
|
||||
>
|
||||
Discord</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={import.meta.env.VITE_BUGS_URL} rel="noreferrer" target="_blank"
|
||||
><span class="icon">bug_report</span> Bugs</a
|
||||
@@ -168,6 +187,11 @@
|
||||
|
||||
$sync-border-radius: 16px;
|
||||
|
||||
.discord-icon {
|
||||
margin: 5px;
|
||||
inline-size: 14px;
|
||||
}
|
||||
|
||||
.sync-box {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
: []),
|
||||
],
|
||||
[
|
||||
{
|
||||
href: "/editor/",
|
||||
icon: "playground_2",
|
||||
title: "Emulator",
|
||||
},
|
||||
{
|
||||
href: import.meta.env.VITE_LEARN_URL,
|
||||
icon: "school",
|
||||
|
||||
@@ -5,18 +5,17 @@
|
||||
import TrackChords from "$lib/charrecorder/TrackChords.svelte";
|
||||
import TrackRollingWpm from "$lib/charrecorder/TrackRollingWpm.svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import { initSerial, serialPort } from "$lib/serial/connection";
|
||||
import { tick } from "svelte";
|
||||
import { ccosKeyInterceptor } from "$lib/ccos/attachment";
|
||||
|
||||
let recorder: ReplayRecorder = $state(new ReplayRecorder());
|
||||
let replay: Replay | undefined = $state();
|
||||
|
||||
let wpm = $state(0);
|
||||
let cc0Loading = $state(false);
|
||||
let chords: InferredChord[] = $state([]);
|
||||
|
||||
function handleRawKey(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
keyEvent(event);
|
||||
}
|
||||
|
||||
function keyEvent(event: KeyboardEvent) {
|
||||
if (event.key === "Tab") {
|
||||
clear();
|
||||
@@ -47,15 +46,60 @@
|
||||
a.download = "replay.json";
|
||||
a.click();
|
||||
}
|
||||
|
||||
async function connectCC0(event: MouseEvent) {
|
||||
cc0Loading = true;
|
||||
try {
|
||||
await tick();
|
||||
if ($serialPort) {
|
||||
$serialPort?.close();
|
||||
$serialPort = undefined;
|
||||
}
|
||||
const { fetchCCOS } = await import("$lib/ccos/ccos");
|
||||
const ccos = await fetchCCOS();
|
||||
if (ccos) {
|
||||
try {
|
||||
await initSerial(ccos, !event.shiftKey);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cc0Loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Editor</title>
|
||||
</svelte:head>
|
||||
<svelte:window onkeydown={handleRawKey} onkeyup={handleRawKey} />
|
||||
|
||||
<section>
|
||||
<h2>Editor</h2>
|
||||
<h2>
|
||||
CCOS Emulator
|
||||
{#if $serialPort?.chipset === "WASM"}
|
||||
<small>(Emulator Active)</small>
|
||||
{:else}
|
||||
<button class="primary" disabled={cc0Loading} onclick={connectCC0}>
|
||||
<span class="icon">play_arrow</span>
|
||||
Boot CCOS Emulator</button
|
||||
>
|
||||
{/if}
|
||||
</h2>
|
||||
|
||||
<p style:max-width="600px">
|
||||
Try a (limited) demo of CCOS running directly in your browser.<br /><span
|
||||
style:color="var(--md-sys-color-primary)"
|
||||
>Chording requires an <b>NKRO Keyboard</b> to work properly.</span
|
||||
>
|
||||
<br />Browsers usually report key timings with limited accuracy to revent
|
||||
fingerprinting, which can impact chording.
|
||||
<br /><i>Results may vary.</i>
|
||||
<br />
|
||||
Use sidebar tabs to configure <a href="/config/chords/">Chords</a>,
|
||||
<a href="/config/layout/">Layout</a>
|
||||
and <a href="/config/settings/">Settings</a>.
|
||||
</p>
|
||||
|
||||
{#if replay}
|
||||
<div class="replay" transition:fade={{ duration: 100 }}>
|
||||
@@ -66,7 +110,9 @@
|
||||
{#key recorder}
|
||||
<div
|
||||
class="editor"
|
||||
tabindex="-1"
|
||||
out:fade={{ duration: 100 }}
|
||||
{@attach ccosKeyInterceptor($serialPort, recorder)}
|
||||
style:opacity={replay ? 0 : undefined}
|
||||
>
|
||||
<CharRecorder replay={recorder.player} cursor={true} keys={true}>
|
||||
@@ -95,15 +141,38 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
small {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--md-sys-color-primary);
|
||||
font-weight: 500;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
display: inline-flex;
|
||||
background: none;
|
||||
color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.replay,
|
||||
.editor {
|
||||
position: absolute;
|
||||
top: 3em;
|
||||
left: 0;
|
||||
transition: opacity 0.1s;
|
||||
margin: 4px;
|
||||
outline: 1px solid var(--md-sys-color-outline);
|
||||
padding: 16px;
|
||||
padding-bottom: 5em;
|
||||
padding-left: 0;
|
||||
|
||||
&:focus-within {
|
||||
outline: 2px solid var(--md-sys-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
|
||||
@@ -26,6 +26,7 @@ process.env["VITE_LATEST_FIRMWARE"] = "1.1.4";
|
||||
process.env["VITE_STORE_URL"] = "https://www.charachorder.com/";
|
||||
process.env["VITE_MATRIX_URL"] = "https://charachorder.io/";
|
||||
process.env["VITE_FIRMWARE_URL"] = "https://charachorder.io/firmware";
|
||||
process.env["VITE_DISCORD_URL"] = "https://discord.gg/CharaChorder";
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user