mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-02-18 23:22:40 +00:00
feat: ccos emulator
This commit is contained in:
@@ -57,6 +57,7 @@ const config = {
|
|||||||
"graphic_eq",
|
"graphic_eq",
|
||||||
"mail",
|
"mail",
|
||||||
"calculate",
|
"calculate",
|
||||||
|
"playground_2",
|
||||||
"open_in_browser",
|
"open_in_browser",
|
||||||
"chevron_backward",
|
"chevron_backward",
|
||||||
"chevron_forward",
|
"chevron_forward",
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
import type { Attachment } from "svelte/attachments";
|
import type { Attachment } from "svelte/attachments";
|
||||||
import { browser } from "$app/environment";
|
import type { CharaDevice } from "$lib/serial/device";
|
||||||
import { persistentWritable } from "$lib/storage";
|
import type { CCOS, CCOSKeyboardEvent } from "./ccos";
|
||||||
|
import type { ReplayRecorder } from "$lib/charrecorder/core/recorder";
|
||||||
|
|
||||||
export const emulatedCCOS = persistentWritable("emulatedCCOS", false);
|
export function ccosKeyInterceptor(
|
||||||
|
port: CharaDevice | undefined,
|
||||||
export function ccosKeyInterceptor() {
|
recorder: ReplayRecorder,
|
||||||
return ((element: Window) => {
|
) {
|
||||||
const ccos = browser
|
return ((element: HTMLElement) => {
|
||||||
? import("./ccos").then((module) => module.fetchCCOS(".test"))
|
const ccos =
|
||||||
: Promise.resolve(undefined);
|
port?.port && "handleKeyEvent" in port?.port
|
||||||
|
? (port.port as CCOS)
|
||||||
|
: undefined;
|
||||||
|
console.log("Attaching CCOS key interceptor", ccos);
|
||||||
|
|
||||||
function onEvent(event: KeyboardEvent) {
|
function onEvent(event: KeyboardEvent) {
|
||||||
ccos.then((it) => it?.handleKeyEvent(event));
|
ccos?.handleKeyEvent(event);
|
||||||
|
if (!event.defaultPrevented) {
|
||||||
|
recorder.next(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
element.addEventListener("keydown", onEvent, true);
|
if (ccos) {
|
||||||
element.addEventListener("keyup", onEvent, true);
|
element.addEventListener("keydown", onEvent, true);
|
||||||
|
element.addEventListener("keyup", onEvent, true);
|
||||||
|
element.add;
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ccos.then((it) => it?.destroy());
|
|
||||||
element.removeEventListener("keydown", onEvent, true);
|
element.removeEventListener("keydown", onEvent, true);
|
||||||
element.removeEventListener("keyup", 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 { getMeta } from "$lib/meta/meta-storage";
|
||||||
import type { SerialPortLike } from "$lib/serial/device";
|
import type { SerialPortLike } from "$lib/serial/device";
|
||||||
import type {
|
import type {
|
||||||
CCOSInEvent,
|
|
||||||
CCOSInitEvent,
|
CCOSInitEvent,
|
||||||
CCOSKeyPressEvent,
|
CCOSKeyPressEvent,
|
||||||
CCOSKeyReleaseEvent,
|
CCOSKeyReleaseEvent,
|
||||||
@@ -11,7 +10,7 @@ import { KEYCODE_TO_SCANCODE, SCANCODE_TO_KEYCODE } from "./ccos-interop";
|
|||||||
|
|
||||||
const device = "zero_wasm";
|
const device = "zero_wasm";
|
||||||
|
|
||||||
class CCOSKeyboardEvent extends KeyboardEvent {
|
export class CCOSKeyboardEvent extends KeyboardEvent {
|
||||||
constructor(...params: ConstructorParameters<typeof KeyboardEvent>) {
|
constructor(...params: ConstructorParameters<typeof KeyboardEvent>) {
|
||||||
super(...params);
|
super(...params);
|
||||||
}
|
}
|
||||||
@@ -26,7 +25,46 @@ const MASK_GUI = 0b1000_1000;
|
|||||||
export class CCOS implements SerialPortLike {
|
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>([
|
||||||
|
...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" });
|
private readonly worker = new Worker("/ccos-worker.js", { type: "module" });
|
||||||
|
|
||||||
@@ -126,7 +164,6 @@ export class CCOS implements SerialPortLike {
|
|||||||
this.controller?.enqueue(event.data);
|
this.controller?.enqueue(event.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("CCOS worker message", event.data);
|
|
||||||
switch (event.data.type) {
|
switch (event.data.type) {
|
||||||
case "ready": {
|
case "ready": {
|
||||||
this.resolveReady();
|
this.resolveReady();
|
||||||
@@ -220,7 +257,7 @@ export class CCOS implements SerialPortLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCCOS(
|
export async function fetchCCOS(
|
||||||
version = ".2.2.0-beta.12+266bdda",
|
version = "3.0.0-rc.0",
|
||||||
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);
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export class CharaDevice {
|
|||||||
version!: string;
|
version!: string;
|
||||||
company!: "CHARACHORDER" | "FORGE";
|
company!: "CHARACHORDER" | "FORGE";
|
||||||
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G" | "ENGINE" | "ZERO";
|
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G" | "ENGINE" | "ZERO";
|
||||||
chipset!: "M0" | "S2" | "S3";
|
chipset!: "M0" | "S2" | "S3" | "WASM";
|
||||||
keyCount!: 90 | 67 | 256;
|
keyCount!: 90 | 67 | 256;
|
||||||
layerCount = 3;
|
layerCount = 3;
|
||||||
profileCount = 1;
|
profileCount = 1;
|
||||||
@@ -157,7 +157,7 @@ export class CharaDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly port: SerialPortLike,
|
readonly port: SerialPortLike,
|
||||||
public baudRate = 115200,
|
public baudRate = 115200,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -46,15 +46,6 @@
|
|||||||
element?.closest<HTMLElement>("[popover]")?.hidePopover();
|
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) {
|
async function connectDevice(event: MouseEvent) {
|
||||||
const port = await navigator.serial.requestPort({
|
const port = await navigator.serial.requestPort({
|
||||||
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
|
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
|
||||||
|
|||||||
@@ -116,6 +116,25 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<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>
|
<li>
|
||||||
<a href={import.meta.env.VITE_BUGS_URL} rel="noreferrer" target="_blank"
|
<a href={import.meta.env.VITE_BUGS_URL} rel="noreferrer" target="_blank"
|
||||||
><span class="icon">bug_report</span> Bugs</a
|
><span class="icon">bug_report</span> Bugs</a
|
||||||
@@ -168,6 +187,11 @@
|
|||||||
|
|
||||||
$sync-border-radius: 16px;
|
$sync-border-radius: 16px;
|
||||||
|
|
||||||
|
.discord-icon {
|
||||||
|
margin: 5px;
|
||||||
|
inline-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.sync-box {
|
.sync-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
href: "/editor/",
|
||||||
|
icon: "playground_2",
|
||||||
|
title: "Emulator",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: import.meta.env.VITE_LEARN_URL,
|
href: import.meta.env.VITE_LEARN_URL,
|
||||||
icon: "school",
|
icon: "school",
|
||||||
|
|||||||
@@ -5,18 +5,17 @@
|
|||||||
import TrackChords from "$lib/charrecorder/TrackChords.svelte";
|
import TrackChords from "$lib/charrecorder/TrackChords.svelte";
|
||||||
import TrackRollingWpm from "$lib/charrecorder/TrackRollingWpm.svelte";
|
import TrackRollingWpm from "$lib/charrecorder/TrackRollingWpm.svelte";
|
||||||
import { fade } from "svelte/transition";
|
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 recorder: ReplayRecorder = $state(new ReplayRecorder());
|
||||||
let replay: Replay | undefined = $state();
|
let replay: Replay | undefined = $state();
|
||||||
|
|
||||||
let wpm = $state(0);
|
let wpm = $state(0);
|
||||||
|
let cc0Loading = $state(false);
|
||||||
let chords: InferredChord[] = $state([]);
|
let chords: InferredChord[] = $state([]);
|
||||||
|
|
||||||
function handleRawKey(event: KeyboardEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
keyEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyEvent(event: KeyboardEvent) {
|
function keyEvent(event: KeyboardEvent) {
|
||||||
if (event.key === "Tab") {
|
if (event.key === "Tab") {
|
||||||
clear();
|
clear();
|
||||||
@@ -47,15 +46,60 @@
|
|||||||
a.download = "replay.json";
|
a.download = "replay.json";
|
||||||
a.click();
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Editor</title>
|
<title>Editor</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
<svelte:window onkeydown={handleRawKey} onkeyup={handleRawKey} />
|
|
||||||
|
|
||||||
<section>
|
<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}
|
{#if replay}
|
||||||
<div class="replay" transition:fade={{ duration: 100 }}>
|
<div class="replay" transition:fade={{ duration: 100 }}>
|
||||||
@@ -66,7 +110,9 @@
|
|||||||
{#key recorder}
|
{#key recorder}
|
||||||
<div
|
<div
|
||||||
class="editor"
|
class="editor"
|
||||||
|
tabindex="-1"
|
||||||
out:fade={{ duration: 100 }}
|
out:fade={{ duration: 100 }}
|
||||||
|
{@attach ccosKeyInterceptor($serialPort, recorder)}
|
||||||
style:opacity={replay ? 0 : undefined}
|
style:opacity={replay ? 0 : undefined}
|
||||||
>
|
>
|
||||||
<CharRecorder replay={recorder.player} cursor={true} keys={true}>
|
<CharRecorder replay={recorder.player} cursor={true} keys={true}>
|
||||||
@@ -95,15 +141,38 @@
|
|||||||
width: 100%;
|
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,
|
.replay,
|
||||||
.editor {
|
.editor {
|
||||||
position: absolute;
|
|
||||||
top: 3em;
|
|
||||||
left: 0;
|
|
||||||
transition: opacity 0.1s;
|
transition: opacity 0.1s;
|
||||||
|
margin: 4px;
|
||||||
|
outline: 1px solid var(--md-sys-color-outline);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
padding-bottom: 5em;
|
padding-bottom: 5em;
|
||||||
padding-left: 0;
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: 2px solid var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.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_STORE_URL"] = "https://www.charachorder.com/";
|
||||||
process.env["VITE_MATRIX_URL"] = "https://charachorder.io/";
|
process.env["VITE_MATRIX_URL"] = "https://charachorder.io/";
|
||||||
process.env["VITE_FIRMWARE_URL"] = "https://charachorder.io/firmware";
|
process.env["VITE_FIRMWARE_URL"] = "https://charachorder.io/firmware";
|
||||||
|
process.env["VITE_DISCORD_URL"] = "https://discord.gg/CharaChorder";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user