feat: fetch settings from build meta

This commit is contained in:
2025-04-04 18:03:09 +02:00
parent 050af564ab
commit e85a731410
5 changed files with 279 additions and 397 deletions

View File

@@ -1,118 +1,154 @@
settings: - name: spurring
0x1: description: |
title: Enable Serial Header "Chording only" mode which tells your device to output chords on a press
description: boolean 0 or 1, default is 0 rather than a press & release. It also enables you to jump from one
0x2: chord to another without releasing everything and can be activated in
title: Enable Serial Logging GTM or by chording both mirror keys. It can provide significant speed
description: boolean 0 or 1, default is 0 gains with chording, but also takes away the flexibility of character
0x3: entry.
title: Enable Serial Debugging items:
description: boolean 0 or 1, default is 0 - id: 0x41
0x4: name: enable
title: Enable Serial Raw range: [0, 1]
description: boolean 0 or 1, default is 0 - id: 0x43
0x5: name: character counter timeout
title: Enable Serial Chord range: [0, 240000]
description: boolean 0 or 1, default is 0 step: 1000
0x6: scale: 0.001
title: Enable Serial Keyboard unit: s
description: boolean 0 or 1, default is 0 - name: arpeggiates
0x7: description: |
title: Enable Serial Mouse Allows chord modifiers to be hit after instead of with a chord,
description: boolean 0 or 1, default is 0 and enables select keys to be placed before auto-spaces.
0x11: items:
title: Enable USB HID Keyboard - id: 0x51
description: boolean 0 or 1, default is 1 name: enable
0x12: range: [0, 1]
title: Enable Character Entry - id: 0x54
description: boolean 0 or 1 name: timeout
0x13: range: [0, 2550]
title: GUI-CTRL Swap Mode step: 10
description: boolean 0 or 1; 1 swaps keymap 0 and 1. (CCL only) unit: ms
0x14: - name: keyboard
title: Key Scan Duration items:
description: scan rate described in milliseconds; default is 2ms = 500Hz - id: 0x11
0x15: name: enable
title: Key Debounce Press Duration range: [0, 1]
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite - id: 0x12
0x16: name: character entry
title: Key Debounce Release Duration range: [0, 1]
description: debounce time in milliseconds; default is 7ms on the One and 20ms on the Lite - id: 0x13
0x17: name: command option swap
title: Keyboard Output Character Microsecond Delays range: [0, 1]
description: delay time in microseconds (one delay for press and again for release); default is 480us; max is 10240us; increments of 40us description: |
0x21: Swaps ⌥ and ⌘ to make transitioning between Mac and other systems easier.
title: Enable USB HID Mouse - id: 0x14
description: boolean 0 or 1; default is 1 name: poll rate
0x22: range: [0, 255]
title: Slow Mouse Speed unit: Hz
description: pixels to move at the mouse poll rate; default for CC1 is 5 = 250px/s inverse: 1000
0x23: - id: 0x15
title: Fast Mouse Speed name: debounce press
description: pixels to move at the mouse poll rate; default for CC1 is 25 = 1250px/s range: [0, 255]
0x24: unit: ms
title: Enable Active Mouse - id: 0x16
description: boolean 0 or 1; moves mouse back and forth every 60s name: debounce release
0x25: range: [0, 255]
title: Mouse Scroll Speed unit: ms
description: default is 1; polls at 1/4th the rate of the mouse move updates - id: 0x17
0x26: name: output delay
title: Mouse Poll Duration range: [0, 10200]
description: poll rate described in milliseconds; default is 20ms = 50Hz step: 40
0x31: unit: µs
title: Enable Chording - name: mouse
description: boolean 0 or 1 items:
0x32: - id: 0x21
title: Enable Chording Character Counter Timeout name: enable
description: boolean 0 or 1; default is 1 range: [0, 1]
0x33: - id: 0x22
title: Chording Character Counter Timeout Timer name: slow speed
description: 0-255 deciseconds; default is 40 or 4.0 seconds range: [0, 255]
0x34: unit: px
title: Chord Detection Press Tolerance(ms) - id: 0x23
description: 1-50 milliseconds name: fast speed
0x35: range: [0, 255]
title: Chord Detection Release Tolerance(ms) unit: px
description: 1-50 milliseconds - id: 0x24
0x41: name: caffeine
title: Enable Spurring range: [0, 1]
description: boolean 0 or 1; default is 1 description: |
0x42: Keeps computer alive by moving the mouse back and forth one pixel every 60s
title: Enable Spurring Character Counter Timeout - id: 0x25
description: boolean 0 or 1; default is 1 name: scroll speed
0x43: range: [0, 255]
title: Spurring Character Counter Timeout Timer unit: pg
description: 0-255 seconds; default is 240 - id: 0x26
0x51: name: poll rate
title: Enable Arpeggiates range: [0, 255]
description: boolean 0 or 1; default is 1 unit: Hz
0x54: inverse: 1000
title: Arpeggiate Tolerance - name: chording
description: in milliseconds; default 800ms items:
0x61: - id: 0x31
title: Enable Compound Chording (coming soon) name: enable
description: boolean 0 or 1; default is 0 range: [0, 1]
0x64: - id: 0x33
title: Compound Tolerance name: auto delete timeout
description: in milliseconds; default 1500ms range: [0, 25500]
0x81: step: 100
title: LED Brightness - id: 0x34
description: 0-50 (CCL only); default is 5, which draws around 100 mA of current name: press tolerance
0x82: description: |
title: LED Color Code Scales with the number of chord inputs.
description: Color Codes to be listed (CCL only) range: [0, 255]
0x83: unit: ms
title: Enable LED Key Highlight (coming soon) - id: 0x35
description: boolean 0 or 1 (CCL only) name: release tolerance
0x84: description: |
title: Enable LEDs Scales with the number of chord inputs.
description: boolean 0 or 1; default is 1 (CCL only) range: [0, 255]
0x91: unit: ms
title: Operating System - name: leds
description: Operating system codes listed below items:
0x92: - id: 0x84
title: Enable Realtime Feedback name: enable
description: boolean 0 or 1; default is 1 range: [0, 1]
0x93: - id: 0x81
title: Enable CharaChorder Ready on startup name: brightness
description: boolean 0 or 1; default is 1 range: [0, 50]
- id: 0x82
name: base color code
enum:
white: 0
red: 1
orange: 2
yellow: 3
charteuse: 4
green: 5
spring green: 6
cyan: 7
azure: 8
blue: 9
violet: 10
magenta: 11
rose: 12
rainbow: 13
- id: 0x83
name: highlight
range: [0, 1]
- name: misc
items:
- id: 0x91
name: operating system
enum:
windows: 0
mac: 1
linux: 2
ios: 3
android: 4
- id: 0x92
name: GTM realtime feedback
range: [0, 1]
- id: 0x93
name: startup message
range: [0, 1]

View File

@@ -1,4 +1,4 @@
import type { RawVersionMeta, VersionMeta } from "./types/meta"; import type { RawVersionMeta, SettingsMeta, VersionMeta } from "./types/meta";
import type { Listing } from "./types/listing"; import type { Listing } from "./types/listing";
import type { KeymapCategory } from "./types/actions"; import type { KeymapCategory } from "./types/actions";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
@@ -9,7 +9,7 @@ export async function getMeta(
device: string, device: string,
version: string, version: string,
fetch: typeof window.fetch = window.fetch, fetch: typeof window.fetch = window.fetch,
): Promise<VersionMeta | undefined> { ): Promise<VersionMeta> {
while (lock) await lock; while (lock) await lock;
let resolveLock!: () => void; let resolveLock!: () => void;
lock = new Promise((resolve) => (resolveLock = resolve)); lock = new Promise((resolve) => (resolveLock = resolve));
@@ -17,15 +17,19 @@ 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", 1); const dbRequest = indexedDB.open("version-meta", 2);
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);
dbRequest.onupgradeneeded = () => { dbRequest.onupgradeneeded = () => {
const db = dbRequest.result; const db = dbRequest.result;
if (db.objectStoreNames.contains("meta")) {
db.deleteObjectStore("meta");
}
db.createObjectStore("meta", { keyPath: ["device", "version"] }); db.createObjectStore("meta", { keyPath: ["device", "version"] });
}; };
}); });
console.log("upgrading version meta db");
try { try {
const readTransaction = db.transaction(["meta"], "readonly"); const readTransaction = db.transaction(["meta"], "readonly");
@@ -39,7 +43,6 @@ export async function getMeta(
if (item) return item; if (item) return item;
const meta = await fetchMeta(device, version); const meta = await fetchMeta(device, version);
if (!meta) return undefined;
const putTransaction = db.transaction(["meta"], "readwrite"); const putTransaction = db.transaction(["meta"], "readwrite");
const putStore = putTransaction.objectStore("meta"); const putStore = putTransaction.objectStore("meta");
@@ -60,7 +63,7 @@ export async function getMeta(
resolveLock(); resolveLock();
lock = undefined; lock = undefined;
} }
return undefined; return fetchMeta(device, version, fetch);
} }
async function fetchMeta( async function fetchMeta(
@@ -69,7 +72,9 @@ async function fetchMeta(
fetch: typeof window.fetch = window.fetch, fetch: typeof window.fetch = window.fetch,
): Promise<VersionMeta> { ): Promise<VersionMeta> {
const path = `${import.meta.env.VITE_FIRMWARE_URL}/${device}/${version}`; const path = `${import.meta.env.VITE_FIRMWARE_URL}/${device}/${version}`;
const files: Listing[] = await fetch(`${path}/`).then((res) => res.json()); const files: Listing[] = await fetch(`${path}/`)
.then((res) => res.json())
.catch(() => []);
const meta: Partial<RawVersionMeta> | undefined = files.some( const meta: Partial<RawVersionMeta> | undefined = files.some(
(entry) => entry.type === "file" && entry.name === "meta.json", (entry) => entry.type === "file" && entry.name === "meta.json",
) )
@@ -105,6 +110,16 @@ async function fetchMeta(
), ),
} }
: undefined, : undefined,
settings: await (meta?.settings
? fetch(`${path}/${meta.settings}`).then((it) => it.json())
: import("$lib/assets/settings.yml")
.then((it) => (it as any).default)
.then((settings: SettingsMeta[]) => {
if (!device.startsWith("lite_")) {
settings = settings.filter((it) => it.name === "leds");
}
return settings;
})),
actions: await (meta?.actions actions: await (meta?.actions
? fetch(`${path}/${meta.actions}`).then((it) => it.json()) ? fetch(`${path}/${meta.actions}`).then((it) => it.json())
: Promise.all<KeymapCategory[]>( : Promise.all<KeymapCategory[]>(

View File

@@ -5,6 +5,23 @@ import type {
} from "$lib/share/chara-file"; } from "$lib/share/chara-file";
import type { KeymapCategory } from "./actions"; import type { KeymapCategory } from "./actions";
export interface SettingsMeta {
name: string;
description?: string;
items: SettingsItemMeta[];
}
export interface SettingsItemMeta {
id: number;
description?: string;
enum?: string[];
range: [number, number];
step?: number;
unit?: string;
inverse?: number;
scale?: number;
}
export interface RawVersionMeta { export interface RawVersionMeta {
version: string; version: string;
target: string; target: string;
@@ -14,6 +31,7 @@ export interface RawVersionMeta {
public_build: boolean; public_build: boolean;
development_mode: number; development_mode: number;
actions: string; actions: string;
settings: string;
factory_defaults: { factory_defaults: {
layout: string; layout: string;
settings: string; settings: string;
@@ -38,6 +56,7 @@ export interface VersionMeta {
dirty: boolean; dirty: boolean;
developmentBuild: boolean; developmentBuild: boolean;
actions: KeymapCategory[]; actions: KeymapCategory[];
settings: SettingsMeta[];
factoryDefaults?: { factoryDefaults?: {
layout: CharaLayoutFile; layout: CharaLayoutFile;
settings: CharaSettingsFile; settings: CharaSettingsFile;

View File

@@ -5,7 +5,6 @@ import type { Writable } from "svelte/store";
import type { CharaLayout } from "$lib/serialization/layout"; import type { CharaLayout } from "$lib/serialization/layout";
import { persistentWritable } from "$lib/storage"; import { persistentWritable } from "$lib/storage";
import { userPreferences } from "$lib/preferences"; import { userPreferences } from "$lib/preferences";
import settingInfo from "$lib/assets/settings.yml";
import { getMeta } from "$lib/meta/meta-storage"; import { getMeta } from "$lib/meta/meta-storage";
import type { VersionMeta } from "$lib/meta/types/meta"; import type { VersionMeta } from "$lib/meta/types/meta";
@@ -69,19 +68,19 @@ export async function initSerial(manual = false, withSync = true) {
export async function sync() { export async function sync() {
const device = get(serialPort); const device = get(serialPort);
if (!device) return; if (!device) return;
getMeta( syncStatus.set("downloading");
const meta = await getMeta(
`${device.device}_${device.chipset}`.toLowerCase(), `${device.device}_${device.chipset}`.toLowerCase(),
device.version.toString(), device.version.toString(),
).then((meta) => { );
deviceMeta.set(meta); deviceMeta.set(meta);
});
const chordCount = await device.getChordCount(); const chordCount = await device.getChordCount();
syncStatus.set("downloading");
const max = const maxSettings = meta.settings
Object.keys(settingInfo["settings"]).length + .map((it) => it.items.length)
device.keyCount * 3 + .reduce((a, b) => a + b, 0);
chordCount;
const max = maxSettings + device.keyCount * 3 + chordCount;
let current = 0; let current = 0;
syncProgress.set({ max, current }); syncProgress.set({ max, current });
function progressTick() { function progressTick() {
@@ -90,12 +89,12 @@ export async function sync() {
} }
const parsedSettings: number[] = []; const parsedSettings: number[] = [];
for (const key in settingInfo["settings"]) { for (const category of meta.settings) {
try { for (const setting of category.items) {
parsedSettings[Number.parseInt(key)] = await device.getSetting( try {
Number.parseInt(key), parsedSettings[setting.id] = await device.getSetting(setting.id);
); } catch {}
} catch {} }
progressTick(); progressTick();
} }
deviceSettings.set(parsedSettings); deviceSettings.set(parsedSettings);

View File

@@ -17,6 +17,21 @@
import { preference } from "$lib/preferences"; import { preference } from "$lib/preferences";
import { action } from "$lib/title"; import { action } from "$lib/title";
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import type { SettingsItemMeta } from "$lib/meta/types/meta";
function titlecase(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function settingValue(value: number, setting: SettingsItemMeta) {
if (setting.inverse !== undefined) {
return setting.inverse / value;
}
if (setting.scale !== undefined) {
return value * setting.scale;
}
return value;
}
</script> </script>
<svelte:head> <svelte:head>
@@ -71,15 +86,6 @@
/></label /></label
> >
{#if $serialPort} {#if $serialPort}
<label
>Boot message<input type="checkbox" use:setting={{ id: 0x93 }} /></label
>
<label
>GTM Realtime Feedback<input
type="checkbox"
use:setting={{ id: 0x92 }}
/></label
>
{#if $deviceMeta?.factoryDefaults?.settings} {#if $deviceMeta?.factoryDefaults?.settings}
<button <button
use:action={{ title: "Reset Settings" }} use:action={{ title: "Reset Settings" }}
@@ -91,256 +97,63 @@
<button class="outline" use:popup={ResetPopup}>Recovery...</button> <button class="outline" use:popup={ResetPopup}>Recovery...</button>
{/if} {/if}
</fieldset> </fieldset>
{#if $deviceMeta}
{#if $serialPort} {#each $deviceMeta.settings as category}
<fieldset>
<legend
><label
><input type="checkbox" use:setting={{ id: 0x41 }} />Spurring</label
></legend
>
<p>
"Chording only" mode which tells your device to output chords on a press
rather than a press & release. It also enables you to jump from one
chord to another without releasing everything and can be activated in
GTM or by chording both mirror keys. It can provide significant speed
gains with chording, but also takes away the flexibility of character
entry.
</p>
<p>
Spurring also helps new users learn how to chord by eliminating the need
to focus on timing.
</p>
<p>
Spurring is toggled by chording <Action display="keys" action={540} /> and
<Action display="keys" action={542} /> together.
</p>
<label
>Character Counter Timeout<span class="unit"
><input
type="number"
step="0.001"
min="0"
max="240"
use:setting={{ id: 0x43, scale: 0.001 }}
/>s</span
></label
>
</fieldset>
<fieldset>
<legend
><label
><input
type="checkbox"
use:setting={{ id: 0x51 }}
/>Arpeggiates</label
></legend
>
<p>
A quick, single key press and release used to indicate a suffix, prefix,
or modifier to be associated with a chord.
</p>
<p>
The following keys have special behavior when arpeggiates are enabled:
</p>
<ul>
<li>
<Action display="keys" action={44} />, <Action
display="keys"
action={59}
/> and <Action display="keys" action={58} /> will be placed before the
auto-inserted space
</li>
<li>
<Action display="keys" action={46} />, <Action
display="keys"
action={63}
/> and <Action display="keys" action={33} /> will be placed before the
auto-inserted space and capitalize the next word
</li>
<li>
<Action display="keys" action={45} /> and <Action
display="keys"
action={47}
/> will replace the auto-inserted space
</li>
</ul>
<label
>Timeout After Chord<span class="unit"
><input type="number" step="1" use:setting={{ id: 0x54 }} />ms</span
></label
>
</fieldset>
<fieldset>
<legend>Chord Modifiers</legend>
<p>
Chord modifiers change a chord when held with the chord or when pressed
after (arpeggiated), <b>provided that arpeggiates are enabled.</b>
</p>
<ul>
<li>
<Action display="keys" action={513} /> Capitalizes the first letter of
a chord
</li>
<li>
<Action display="keys" action={540} /> Present Tense (supported words only)
</li>
<li>
<Action display="keys" action={542} /> Plural (supported words only)
</li>
<li>
<Action display="keys" action={550} /> Past Tense (supported words only)
</li>
<li>
<Action display="keys" action={551} /> Comparative (supported words only)
</li>
</ul>
</fieldset>
<fieldset>
<legend>Character Entry</legend>
{#if $serialPort.device === "LITE"}
<label
>Swap Keymap 0 and 1<input
type="checkbox"
use:setting={{ id: 0x13 }}
/></label
>
{/if}
<label>
Character Entry (chentry)
<input type="checkbox" use:setting={{ id: 0x12 }} />
</label>
<label>
Key Scan Rate
<span class="unit"
><input
type="number"
use:setting={{ id: 0x14, inverse: 1000 }}
/>Hz</span
></label
>
<label>
Key Debounce Press<span class="unit"
><input type="number" use:setting={{ id: 0x15 }} />ms</span
></label
>
<label
>Key Debounce Release<span class="unit"
><input type="number" use:setting={{ id: 0x16 }} />ms</span
></label
>
<label
>Output Character Delay<span class="unit"
><input type="number" use:setting={{ id: 0x17 }} />µs</span
></label
>
</fieldset>
<fieldset>
<legend
><label><input type="checkbox" use:setting={{ id: 0x21 }} />Mouse</label
></legend
>
<label
>Mouse Speed<input type="number" use:setting={{ id: 0x22 }} /><input
type="number"
use:setting={{ id: 0x23 }}
/></label
>
<label
>Scroll Speed<input type="number" use:setting={{ id: 0x25 }} /></label
>
<label>
<span>
Active Mouse
<p>Bounces mouse by 1px every 60s if enabled</p></span
>
<input type="checkbox" use:setting={{ id: 0x24 }} /></label
>
<label
>Poll Rate<span class="unit"
><input
type="number"
use:setting={{ id: 0x26, inverse: 1000 }}
/>Hz</span
></label
>
</fieldset>
<fieldset>
<legend
><label
><input type="checkbox" use:setting={{ id: 0x31 }} />Chording</label
></legend
>
<label
>Auto-delete Timeout <span class="unit"
><input
type="number"
min="0"
max="25500"
step="10"
use:setting={{ id: 0x33 }}
/>ms</span
></label
>
<label
>Press Tolerance<span class="unit"
><input
type="number"
min="1"
max="150"
step="1"
use:setting={{ id: 0x34 }}
/>ms</span
></label
>
<label
>Release Tolerance<span class="unit"
><input
type="number"
min="1"
max="150"
step="1"
use:setting={{ id: 0x35 }}
/>ms</span
></label
>
</fieldset>
{#if $serialPort.device === "LITE"}
<fieldset> <fieldset>
<legend <legend>
><label><input type="checkbox" use:setting={{ id: 0x84 }} />RGB</label {#if category.items[0]?.name === "enable"}
></legend <label
> ><input
<label type="checkbox"
>Brightness<input use:setting={{ id: category.items[0].id }}
use:setting={{ id: 0x81 }} />{titlecase(category.name)}</label
type="number" >
min="0" {:else}
max="50" {titlecase(category.name)}
step="1" {/if}
/></label </legend>
> {#if category.description}
<select use:setting={{ id: 0x82 }}> <p>{category.description}</p>
<option value="0">White</option> {/if}
<option value="1">Red</option> {#each category.items as item}
<option value="2">Orange</option> {#if item.name !== "enable"}
<option value="3">Yellow</option> <label
<option value="4">Lime</option> >{#if item.enum}
<option value="5">Green</option> <select use:setting={{ id: item.id }}>
<option value="7">Cyan</option> {#each item.enum as name, value}
<option value="9">Blue</option> <option {value}>{titlecase(name)}</option>
<option value="10">Violet</option> {/each}
<option value="11">Pink</option> </select>
<option value="13">Multicolor</option> {:else if item.range[0] === 0 && item.range[1] === 1}
</select> <input type="checkbox" use:setting={{ id: item.id }} />
{:else}
<span class="unit"
><input
type="number"
min={settingValue(item.range[0], item)}
max={settingValue(item.range[1], item)}
step={settingValue(item.step, item)}
use:setting={{
id: item.id,
inverse: item.inverse,
scale: item.scale,
}}
/>{item.unit}</span
>
{/if}
{#if item.description}
<span
>{titlecase(item.name)}
<p>{item.description}</p></span
>
{:else}
{titlecase(item.name)}
{/if}
</label>
{/if}
{/each}
</fieldset> </fieldset>
{/if} {/each}
{/if} {/if}
</section> </section>