Skip to content

Commit 16bf766

Browse files
committed
feat: ccos emulator
1 parent ee8d400 commit 16bf766

File tree

9 files changed

+178
-41
lines changed

9 files changed

+178
-41
lines changed

icons.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const config = {
5757
"graphic_eq",
5858
"mail",
5959
"calculate",
60+
"playground_2",
6061
"open_in_browser",
6162
"chevron_backward",
6263
"chevron_forward",

src/lib/ccos/attachment.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
11
import type { Attachment } from "svelte/attachments";
2-
import { browser } from "$app/environment";
3-
import { persistentWritable } from "$lib/storage";
2+
import type { CharaDevice } from "$lib/serial/device";
3+
import type { CCOS, CCOSKeyboardEvent } from "./ccos";
4+
import type { ReplayRecorder } from "$lib/charrecorder/core/recorder";
45

5-
export const emulatedCCOS = persistentWritable("emulatedCCOS", false);
6-
7-
export function ccosKeyInterceptor() {
8-
return ((element: Window) => {
9-
const ccos = browser
10-
? import("./ccos").then((module) => module.fetchCCOS(".test"))
11-
: Promise.resolve(undefined);
6+
export function ccosKeyInterceptor(
7+
port: CharaDevice | undefined,
8+
recorder: ReplayRecorder,
9+
) {
10+
return ((element: HTMLElement) => {
11+
const ccos =
12+
port?.port && "handleKeyEvent" in port?.port
13+
? (port.port as CCOS)
14+
: undefined;
15+
console.log("Attaching CCOS key interceptor", ccos);
1216

1317
function onEvent(event: KeyboardEvent) {
14-
ccos.then((it) => it?.handleKeyEvent(event));
18+
ccos?.handleKeyEvent(event);
19+
if (!event.defaultPrevented) {
20+
recorder.next(event);
21+
}
1522
}
1623

17-
element.addEventListener("keydown", onEvent, true);
18-
element.addEventListener("keyup", onEvent, true);
24+
if (ccos) {
25+
element.addEventListener("keydown", onEvent, true);
26+
element.addEventListener("keyup", onEvent, true);
27+
element.add;
28+
}
1929

2030
return () => {
21-
ccos.then((it) => it?.destroy());
2231
element.removeEventListener("keydown", onEvent, true);
2332
element.removeEventListener("keyup", onEvent, true);
2433
};
25-
}) satisfies Attachment<Window>;
34+
}) satisfies Attachment<HTMLElement>;
2635
}

src/lib/ccos/ccos.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { getMeta } from "$lib/meta/meta-storage";
22
import type { SerialPortLike } from "$lib/serial/device";
33
import type {
4-
CCOSInEvent,
54
CCOSInitEvent,
65
CCOSKeyPressEvent,
76
CCOSKeyReleaseEvent,
@@ -11,7 +10,7 @@ import { KEYCODE_TO_SCANCODE, SCANCODE_TO_KEYCODE } from "./ccos-interop";
1110

1211
const device = "zero_wasm";
1312

14-
class CCOSKeyboardEvent extends KeyboardEvent {
13+
export class CCOSKeyboardEvent extends KeyboardEvent {
1514
constructor(...params: ConstructorParameters<typeof KeyboardEvent>) {
1615
super(...params);
1716
}
@@ -26,7 +25,46 @@ const MASK_GUI = 0b1000_1000;
2625
export class CCOS implements SerialPortLike {
2726
private readonly currKeys = new Set<number>();
2827

29-
private readonly layout = new Map<string, string>();
28+
private readonly layout = new Map<string, string>([
29+
...Array.from(
30+
{ length: 26 },
31+
(_, i) =>
32+
[
33+
JSON.stringify([`Key${String.fromCharCode(65 + i)}`, "Shift"]),
34+
String.fromCharCode(65 + i),
35+
] as const,
36+
),
37+
...Array.from(
38+
{ length: 10 },
39+
(_, i) => [JSON.stringify([`Key${i}`]), i.toString()] as const,
40+
),
41+
42+
[JSON.stringify(["Space"]), " "],
43+
[JSON.stringify(["Backquote"]), "`"],
44+
[JSON.stringify(["Minus"]), "-"],
45+
[JSON.stringify(["Comma"]), ","],
46+
[JSON.stringify(["Period"]), "."],
47+
[JSON.stringify(["Semicolon"]), ";"],
48+
[JSON.stringify(["Equal"]), "="],
49+
50+
[JSON.stringify(["Backquote", "Shift"]), "~"],
51+
[JSON.stringify(["Minus", "Shift"]), "_"],
52+
[JSON.stringify(["Comma", "Shift"]), "<"],
53+
[JSON.stringify(["Period", "Shift"]), ">"],
54+
[JSON.stringify(["Semicolon", "Shift"]), ":"],
55+
[JSON.stringify(["Equal", "Shift"]), "+"],
56+
57+
[JSON.stringify(["Digit0", "Shift"]), ")"],
58+
[JSON.stringify(["Digit1", "Shift"]), "!"],
59+
[JSON.stringify(["Digit2", "Shift"]), "@"],
60+
[JSON.stringify(["Digit3", "Shift"]), "#"],
61+
[JSON.stringify(["Digit4", "Shift"]), "$"],
62+
[JSON.stringify(["Digit5", "Shift"]), "%"],
63+
[JSON.stringify(["Digit6", "Shift"]), "^"],
64+
[JSON.stringify(["Digit7", "Shift"]), "&"],
65+
[JSON.stringify(["Digit8", "Shift"]), "*"],
66+
[JSON.stringify(["Digit9", "Shift"]), "("],
67+
]);
3068

3169
private readonly worker = new Worker("/ccos-worker.js", { type: "module" });
3270

@@ -126,7 +164,6 @@ export class CCOS implements SerialPortLike {
126164
this.controller?.enqueue(event.data);
127165
return;
128166
}
129-
console.log("CCOS worker message", event.data);
130167
switch (event.data.type) {
131168
case "ready": {
132169
this.resolveReady();
@@ -220,7 +257,7 @@ export class CCOS implements SerialPortLike {
220257
}
221258

222259
export async function fetchCCOS(
223-
version = ".2.2.0-beta.12+266bdda",
260+
version = "3.0.0-rc.0",
224261
fetch: typeof window.fetch = window.fetch,
225262
): Promise<CCOS | undefined> {
226263
const meta = await getMeta(device, version, fetch);

src/lib/serial/device.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export class CharaDevice {
147147
version!: string;
148148
company!: "CHARACHORDER" | "FORGE";
149149
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G" | "ENGINE" | "ZERO";
150-
chipset!: "M0" | "S2" | "S3";
150+
chipset!: "M0" | "S2" | "S3" | "WASM";
151151
keyCount!: 90 | 67 | 256;
152152
layerCount = 3;
153153
profileCount = 1;
@@ -157,7 +157,7 @@ export class CharaDevice {
157157
}
158158

159159
constructor(
160-
private readonly port: SerialPortLike,
160+
readonly port: SerialPortLike,
161161
public baudRate = 115200,
162162
) {}
163163

src/routes/(app)/ConnectPopup.svelte

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,6 @@
4646
element?.closest<HTMLElement>("[popover]")?.hidePopover();
4747
}
4848
49-
async function connectCC0(event: MouseEvent) {
50-
const { fetchCCOS } = await import("$lib/ccos/ccos");
51-
closePopover();
52-
const ccos = await fetchCCOS();
53-
if (ccos) {
54-
connect(ccos, !event.shiftKey);
55-
}
56-
}
57-
5849
async function connectDevice(event: MouseEvent) {
5950
const port = await navigator.serial.requestPort({
6051
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],

src/routes/(app)/Footer.svelte

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,25 @@
116116
{/if}
117117
</div>
118118
<ul>
119+
<li>
120+
<a
121+
href={import.meta.env.VITE_DISCORD_URL}
122+
rel="noreferrer"
123+
target="_blank"
124+
>
125+
<svg
126+
class="discord-icon"
127+
xmlns="http://www.w3.org/2000/svg"
128+
viewBox="0 0 126.64 96"
129+
>
130+
<path
131+
fill="currentColor"
132+
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"
133+
/></svg
134+
>
135+
Discord</a
136+
>
137+
</li>
119138
<li>
120139
<a href={import.meta.env.VITE_BUGS_URL} rel="noreferrer" target="_blank"
121140
><span class="icon">bug_report</span> Bugs</a
@@ -168,6 +187,11 @@
168187
169188
$sync-border-radius: 16px;
170189
190+
.discord-icon {
191+
margin: 5px;
192+
inline-size: 14px;
193+
}
194+
171195
.sync-box {
172196
display: flex;
173197
position: relative;

src/routes/(app)/Sidebar.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
: []),
1818
],
1919
[
20+
{
21+
href: "/editor/",
22+
icon: "playground_2",
23+
title: "Emulator",
24+
},
2025
{
2126
href: import.meta.env.VITE_LEARN_URL,
2227
icon: "school",

src/routes/(app)/editor/+page.svelte

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@
55
import TrackChords from "$lib/charrecorder/TrackChords.svelte";
66
import TrackRollingWpm from "$lib/charrecorder/TrackRollingWpm.svelte";
77
import { fade } from "svelte/transition";
8+
import { initSerial, serialPort } from "$lib/serial/connection";
9+
import { tick } from "svelte";
10+
import { ccosKeyInterceptor } from "$lib/ccos/attachment";
811
912
let recorder: ReplayRecorder = $state(new ReplayRecorder());
1013
let replay: Replay | undefined = $state();
1114
1215
let wpm = $state(0);
16+
let cc0Loading = $state(false);
1317
let chords: InferredChord[] = $state([]);
1418
15-
function handleRawKey(event: KeyboardEvent) {
16-
event.preventDefault();
17-
keyEvent(event);
18-
}
19-
2019
function keyEvent(event: KeyboardEvent) {
2120
if (event.key === "Tab") {
2221
clear();
@@ -47,15 +46,60 @@
4746
a.download = "replay.json";
4847
a.click();
4948
}
49+
50+
async function connectCC0(event: MouseEvent) {
51+
cc0Loading = true;
52+
try {
53+
await tick();
54+
if ($serialPort) {
55+
$serialPort?.close();
56+
$serialPort = undefined;
57+
}
58+
const { fetchCCOS } = await import("$lib/ccos/ccos");
59+
const ccos = await fetchCCOS();
60+
if (ccos) {
61+
try {
62+
await initSerial(ccos, !event.shiftKey);
63+
} catch (error) {
64+
console.error(error);
65+
}
66+
}
67+
} finally {
68+
cc0Loading = false;
69+
}
70+
}
5071
</script>
5172

5273
<svelte:head>
5374
<title>Editor</title>
5475
</svelte:head>
55-
<svelte:window onkeydown={handleRawKey} onkeyup={handleRawKey} />
5676

5777
<section>
58-
<h2>Editor</h2>
78+
<h2>
79+
CCOS Emulator
80+
{#if $serialPort?.chipset === "WASM"}
81+
<small>(Emulator Active)</small>
82+
{:else}
83+
<button class="primary" disabled={cc0Loading} onclick={connectCC0}>
84+
<span class="icon">play_arrow</span>
85+
Boot CCOS Emulator</button
86+
>
87+
{/if}
88+
</h2>
89+
90+
<p style:max-width="600px">
91+
Try a (limited) demo of CCOS running directly in your browser.<br /><span
92+
style:color="var(--md-sys-color-primary)"
93+
>Chording requires an <b>NKRO Keyboard</b> to work properly.</span
94+
>
95+
<br />Browsers usually report key timings with limited accuracy to revent
96+
fingerprinting, which can impact chording.
97+
<br /><i>Results may vary.</i>
98+
<br />
99+
Use sidebar tabs to configure <a href="/config/chords/">Chords</a>,
100+
<a href="/config/layout/">Layout</a>
101+
and <a href="/config/settings/">Settings</a>.
102+
</p>
59103

60104
{#if replay}
61105
<div class="replay" transition:fade={{ duration: 100 }}>
@@ -66,7 +110,9 @@
66110
{#key recorder}
67111
<div
68112
class="editor"
113+
tabindex="-1"
69114
out:fade={{ duration: 100 }}
115+
{@attach ccosKeyInterceptor($serialPort, recorder)}
70116
style:opacity={replay ? 0 : undefined}
71117
>
72118
<CharRecorder replay={recorder.player} cursor={true} keys={true}>
@@ -95,15 +141,38 @@
95141
width: 100%;
96142
}
97143
144+
a {
145+
display: inline;
146+
padding: 0;
147+
color: var(--md-sys-color-primary);
148+
}
149+
150+
small {
151+
display: inline-flex;
152+
align-items: center;
153+
gap: 4px;
154+
color: var(--md-sys-color-primary);
155+
font-weight: 500;
156+
font-size: 0.6em;
157+
}
158+
159+
button.primary {
160+
display: inline-flex;
161+
background: none;
162+
color: var(--md-sys-color-primary);
163+
}
164+
98165
.replay,
99166
.editor {
100-
position: absolute;
101-
top: 3em;
102-
left: 0;
103167
transition: opacity 0.1s;
168+
margin: 4px;
169+
outline: 1px solid var(--md-sys-color-outline);
104170
padding: 16px;
105171
padding-bottom: 5em;
106-
padding-left: 0;
172+
173+
&:focus-within {
174+
outline: 2px solid var(--md-sys-color-primary);
175+
}
107176
}
108177
109178
.toolbar {

vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ process.env["VITE_LATEST_FIRMWARE"] = "1.1.4";
2626
process.env["VITE_STORE_URL"] = "https://www.charachorder.com/";
2727
process.env["VITE_MATRIX_URL"] = "https://charachorder.io/";
2828
process.env["VITE_FIRMWARE_URL"] = "https://charachorder.io/firmware";
29+
process.env["VITE_DISCORD_URL"] = "https://discord.gg/CharaChorder";
2930

3031
export default defineConfig({
3132
build: {

0 commit comments

Comments
 (0)