9 Commits

13 changed files with 315 additions and 72 deletions

View File

@@ -20,7 +20,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
version: 9
- name: 🐉 Use Node.js 22.4.x
uses: actions/setup-node@v3
with:

View File

@@ -110,6 +110,9 @@ const config = {
"experiment",
"code",
"dictionary",
"developer_board",
"developer_board_off",
"memory",
],
codePoints: {
speed: "e9e4",

View File

@@ -1,11 +1,11 @@
{
"name": "charachorder-device-manager",
"version": "2.1.0",
"version": "2.2.1",
"license": "AGPL-3.0-or-later",
"private": true,
"engines": {
"node": ">=18.16",
"pnpm": ">=8.6"
"node": ">=22.4",
"pnpm": ">=9.4"
},
"repository": {
"type": "git",
@@ -62,6 +62,7 @@
"codemirror": "^6.0.1",
"cypress": "^13.13.2",
"d3": "^7.9.0",
"esptool-js": "^0.4.7",
"flexsearch": "^0.7.43",
"fontkit": "^2.0.4",
"glob": "^11.0.0",
@@ -89,6 +90,7 @@
"vite-plugin-mkcert": "^1.17.6",
"vite-plugin-pwa": "^0.20.5",
"vitest": "^2.1.4",
"web-serial-polyfill": "^1.0.15",
"workbox-window": "^7.3.0"
},
"type": "module"

30
pnpm-lock.yaml generated
View File

@@ -92,6 +92,9 @@ importers:
d3:
specifier: ^7.9.0
version: 7.9.0
esptool-js:
specifier: ^0.4.7
version: 0.4.7
flexsearch:
specifier: ^0.7.43
version: 0.7.43
@@ -173,6 +176,9 @@ importers:
vitest:
specifier: ^2.1.4
version: 2.1.4(@types/node@20.14.10)(jsdom@25.0.1)(sass@1.80.6)(terser@5.31.1)
web-serial-polyfill:
specifier: ^1.0.15
version: 1.0.15
workbox-window:
specifier: ^7.3.0
version: 7.3.0
@@ -1645,6 +1651,9 @@ packages:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
atob-lite@2.0.0:
resolution: {integrity: sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==}
autoprefixer@10.4.20:
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
engines: {node: ^10 || ^12 || >=14}
@@ -2250,6 +2259,9 @@ packages:
esm-env@1.0.0:
resolution: {integrity: sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==}
esptool-js@0.4.7:
resolution: {integrity: sha512-xVwtSVDRsvjXSEvNFrorgJfB71RFFkZkL+hs7O7gW5hgPrKGywZxo2U5LJddzkJ6eE31QinNVyywc0OaSntZCw==}
esrap@1.2.2:
resolution: {integrity: sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==}
@@ -3084,6 +3096,9 @@ packages:
pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -4060,6 +4075,9 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
web-serial-polyfill@1.0.15:
resolution: {integrity: sha512-usZN7kGRkEWr8DzRWxW+og55L1fHo4hNIwxCSCfWKpM+i0L+2AwzupMvkDFxnJNqUFOhLaD3PlgAOJxUOUrAoA==}
webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
@@ -5796,6 +5814,8 @@ snapshots:
at-least-node@1.0.0: {}
atob-lite@2.0.0: {}
autoprefixer@10.4.20(postcss@8.4.39):
dependencies:
browserslist: 4.24.2
@@ -6519,6 +6539,12 @@ snapshots:
esm-env@1.0.0: {}
esptool-js@0.4.7:
dependencies:
atob-lite: 2.0.0
pako: 2.1.0
tslib: 2.6.3
esrap@1.2.2:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -7336,6 +7362,8 @@ snapshots:
pako@0.2.9: {}
pako@2.1.0: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -8336,6 +8364,8 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
web-serial-polyfill@1.0.15: {}
webidl-conversions@4.0.2: {}
webidl-conversions@7.0.0: {}

View File

@@ -1,6 +1,6 @@
[package]
name = "app"
version = "2.1.0"
version = "2.2.1"
description = "A Tauri App"
authors = ["Thea Schöbl <dev@theaninova.de>"]
license = "AGPL-3"

View File

@@ -6,7 +6,7 @@
"devPath": "http://localhost:5173",
"distDir": "../build"
},
"package": { "productName": "amacc1ng", "version": "2.1.0" },
"package": { "productName": "amacc1ng", "version": "2.2.1" },
"tauri": {
"allowlist": { "all": false },
"bundle": {

View File

@@ -5,20 +5,33 @@ col:
row:
- switch: { e: 26, n: 27, w: 28, s: 29 }
- switch: { e: 21, n: 22, w: 23, s: 24 }
- offset: [4, 0]
switch: { w: 66, n: 67, e: 68, s: 69 }
- switch: { w: 71, n: 72, e: 73, s: 74 }
- offset: [2, 0]
row:
- switch: { e: 41, n: 42, w: 43, s: 44 }
- switch: { e: 36, n: 37, w: 38, s: 39 }
- offset: [4, 0]
switch: { w: 81, n: 82, e: 83, s: 84 }
- switch: { w: 86, n: 87, e: 88, s: 89 }
# Pinkie / Index
- offset: [0, -3]
row:
- switch: { e: 31, n: 32, w: 33, s: 34 }
- offset: [4, 0]
switch: { e: 16, n: 17, w: 18, s: 19 }
- switch: { w: 61, n: 62, e: 63, s: 64 }
- offset: [4, 0]
switch: { w: 76, n: 77, e: 78, s: 79 }
# Thumbs
- row:
- offset: [5.5, 0.5]
switch: { e: 11, n: 12, w: 13, s: 14 }
- offset: [1, 0.5]
switch: { w: 56, n: 57, e: 58, s: 59 }
- row:
- offset: [4.5, -0.25]
switch: { e: 6, n: 7, w: 8, s: 9 }
- offset: [3, -0.25]
switch: { w: 51, n: 52, e: 53, s: 54 }

View File

@@ -1,24 +1,37 @@
name: M4GR
name: M4G
col:
# Ring / Middle
- offset: [2, 0]
row:
- switch: { e: 23, n: 22, w: 21, s: 24 }
- switch: { e: 28, n: 27, w: 26, s: 29 }
- switch: { e: 26, n: 27, w: 28, s: 29 }
- switch: { e: 21, n: 22, w: 23, s: 24 }
- offset: [4, 0]
switch: { w: 66, n: 67, e: 68, s: 69 }
- switch: { w: 71, n: 72, e: 73, s: 74 }
- offset: [2, 0]
row:
- switch: { e: 38, n: 37, w: 36, s: 39 }
- switch: { e: 43, n: 42, w: 41, s: 44 }
- switch: { e: 41, n: 42, w: 43, s: 44 }
- switch: { e: 36, n: 37, w: 38, s: 39 }
- offset: [4, 0]
switch: { w: 81, n: 82, e: 83, s: 84 }
- switch: { w: 86, n: 87, e: 88, s: 89 }
# Pinkie / Index
- offset: [0, -3]
row:
- switch: { e: 18, n: 17, w: 16, s: 19 }
- switch: { e: 31, n: 32, w: 33, s: 34 }
- offset: [4, 0]
switch: { e: 33, n: 32, w: 31, s: 34 }
switch: { e: 16, n: 17, w: 18, s: 19 }
- switch: { w: 61, n: 62, e: 63, s: 64 }
- offset: [4, 0]
switch: { w: 76, n: 77, e: 78, s: 79 }
# Thumbs
- row:
- offset: [0.5, 0.5]
switch: { e: 13, n: 12, w: 11, s: 14 }
- offset: [5.5, 0.5]
switch: { e: 11, n: 12, w: 13, s: 14 }
- offset: [1, 0.5]
switch: { w: 56, n: 57, e: 58, s: 59 }
- row:
- offset: [1.5, -0.25]
switch: { e: 8, n: 7, w: 6, s: 9 }
- offset: [4.5, -0.25]
switch: { e: 6, n: 7, w: 8, s: 9 }
- offset: [3, -0.25]
switch: { w: 51, n: 52, e: 53, s: 54 }

View File

@@ -25,8 +25,8 @@ const KEY_COUNTS = {
TWO: 90,
LITE: 67,
X: 256,
M4G: 64,
M4GR: 64,
M4G: 90,
M4GR: 90,
} as const;
if (
@@ -37,6 +37,13 @@ if (
await import("./tauri-serial");
}
if (browser && navigator.serial === undefined && navigator.usb !== undefined) {
// @ts-expect-error polyfill
navigator.serial = await import("web-serial-polyfill").then(
({ serial }) => serial,
);
}
export async function getViablePorts(): Promise<SerialPort[]> {
return navigator.serial.getPorts().then((ports) =>
ports.filter((it) => {

View File

@@ -2,6 +2,7 @@
import { downloadBackup } from "$lib/backup/backup";
import { initSerial, serialPort } from "$lib/serial/connection";
import { fade, slide } from "svelte/transition";
import type { LoaderOptions, ESPLoader } from "esptool-js";
let { data } = $props();
@@ -9,7 +10,12 @@
let success = $state(false);
let error = $state<Error | undefined>(undefined);
let terminalOutput = $state("");
let step = $state(0);
let eraseAll = $state(false);
let espLoader;
async function update() {
working = true;
@@ -18,7 +24,9 @@
const port = $serialPort!;
$serialPort = undefined;
try {
const file = await fetch(otaUrl!).then((it) => it.blob());
const file = await fetch(
`${data.meta.path}/${data.meta.update.ota!}`,
).then((it) => it.blob());
await port.updateFirmware(file);
@@ -36,18 +44,7 @@
: undefined,
);
let isCorrectDevice = $derived(
currentDevice ? currentDevice === data.device : undefined,
);
let uf2Url = $derived(
data.uf2
? `${import.meta.env.VITE_FIRMWARE_URL}${data.device}/${data.version}/${data.uf2.name}`
: undefined,
);
let otaUrl = $derived(
data.ota
? `${import.meta.env.VITE_FIRMWARE_URL}${data.device}/${data.version}/${data.ota.name}`
: undefined,
currentDevice ? currentDevice === data.meta.target : undefined,
);
/**
@@ -84,10 +81,12 @@
}
async function getFileSystem() {
if (!uf2Url) return;
const uf2Promise = fetch(uf2Url).then((it) => it.blob());
if (!data.meta.update.uf2) return;
const uf2Promise = fetch(
`${data.meta.path}/${data.meta.update.uf2.name}`,
).then((it) => it.blob());
const handle = await window.showSaveFilePicker({
id: `${data.device}-update`,
id: `${data.meta.target}-update`,
suggestedName: "CURRENT.UF2",
excludeAcceptAllOption: true,
types: [
@@ -102,21 +101,104 @@
await uf2.stream().pipeTo(writable);
step = 4;
}
async function espBootloader() {
$serialPort?.forget();
const port = await navigator.serial.requestPort();
port.open({ baudRate: 1200 });
}
async function connectEsp(port: SerialPort): Promise<ESPLoader> {
const esptool = data.meta.update.esptool!;
const { Transport, ESPLoader } = await import("esptool-js");
const espLoader = new ESPLoader({
transport: new Transport(port),
baudrate: 9600, // Number(esptool.baud),
romBaudrate: 9600, // Number(esptool.baud),
debugLogging: true,
terminal: {
clean: () => {
terminalOutput = "";
},
writeLine: (data) => {
terminalOutput += data + "\n";
},
write: (data) => {
terminalOutput += data;
},
},
} satisfies LoaderOptions);
await espLoader.detectChip(esptool.before);
if (!espLoader.IS_STUB) {
await espLoader.runStub();
}
return espLoader;
}
async function flashImages() {
const port = await navigator.serial.requestPort();
try {
const esptool = data.meta.update.esptool!;
espLoader = await connectEsp(port);
const fileArray = await Promise.all(
Object.entries(esptool.files).map(([offset, name]) =>
fetch(`${data.meta.path}/${name}`)
.then((it) => it.blob())
.then((it) => it.text())
.then((it) => ({
address: Number(offset),
data: it,
})),
),
);
await espLoader.writeFlash({
flashSize: esptool.flash_size,
flashMode: esptool.flash_mode,
flashFreq: esptool.flash_freq,
compress: true,
eraseAll,
fileArray,
});
} finally {
port.close();
}
}
async function eraseSPI() {
const port = await navigator.serial.requestPort();
try {
console.log(data.meta);
const spiFlash = data.meta.spi_flash!;
espLoader = await connectEsp(port);
/*espLoader.flashSpiAttach(
(spiFlash.connection.clk << 0) |
(spiFlash.connection.q << 8) |
(spiFlash.connection.d << 16) |
(spiFlash.connection.cs << 24),
);
espLoader.flashId();*/
} finally {
port.close();
}
}
</script>
<div class="container">
<h2>
<a class="inline-link" href="/ccos">CCOS</a> /
<a
href="/ccos/{data.device}"
href="/ccos/{data.meta.target}"
class="device inline-link"
class:correct-device={isCorrectDevice === true}
class:incorrect-device={isCorrectDevice === false}>{data.device}</a
class:incorrect-device={isCorrectDevice === false}>{data.meta.target}</a
>
/ <em class="version">{data.version}</em>
/ <em class="version">{data.meta.version}</em>
</h2>
{#if data.ota && !data.device.endsWith("m0")}
{#if data.meta.update.ota && !data.meta.target.endsWith("m0")}
{@const buttonError = error || (!success && isCorrectDevice === false)}
<section>
<button
@@ -136,7 +218,7 @@
{$serialPort.chipset}</b
>
will be updated from <b class="version">{$serialPort.version}</b> to
<b class="version">{data.version}</b>
<b class="version">{data.meta.version}</b>
</div>
{:else if $serialPort && isCorrectDevice === false}
<div class="error" transition:slide>
@@ -158,27 +240,6 @@
<h3>Manual Update</h3>
{/if}
<ul class="files">
{#if data.uf2}
<li>
<a target="_blank" download href={uf2Url}
>{data.uf2.name} <span class="icon">download</span><span class="size"
>{toByteUnit(data.uf2.size)}</span
></a
>
</li>
{/if}
{#if data.ota}
<li>
<a target="_blank" download href={otaUrl}
>{data.ota.name} <span class="icon">download</span><span class="size"
>{toByteUnit(data.uf2.size)}</span
></a
>
</li>
{/if}
</ul>
{#if isCorrectDevice === false}
<div transition:slide class="incorrect-device">
These files are incompatible with your device
@@ -186,7 +247,6 @@
{/if}
<section>
<h4>UF2 Instructions</h4>
<ol>
<li>
<button class="inline-button" onclick={connect}
@@ -227,6 +287,37 @@
</li>
</ol>
</section>
{#if data.meta.update.esptool}
<section>
<h3>Factory Flash (WIP)</h3>
<p>
If everything else fails, you can go through the same process that is
being used in the factory.
</p>
<p>
This will temporarily brick your device if the process is not done
completely or incorrectly.
</p>
<div class="esp-buttons">
<button onclick={espBootloader}
><span class="icon">memory</span>ESP Bootloader</button
>
<button onclick={flashImages}
><span class="icon">developer_board</span>Flash Images</button
>
<label
><input type="checkbox" id="erase" bind:checked={eraseAll} />Erase All</label
>
<button onclick={eraseSPI}
><span class="icon">developer_board</span>Erase SPI Flash</button
>
</div>
<pre>{terminalOutput}</pre>
</section>
{/if}
</div>
<style lang="scss">
@@ -239,6 +330,10 @@
margin-block-start: 4em;
}
pre {
overflow: auto;
}
.primary {
color: var(--md-sys-color-primary);
}
@@ -249,6 +344,7 @@
.container {
width: calc(min(100%, 16cm));
overflow: auto;
}
@keyframes rotate {
@@ -402,4 +498,8 @@
.incorrect-device {
color: var(--md-sys-color-error);
}
.esp-buttons {
display: flex;
}
</style>

View File

@@ -1,20 +1,50 @@
import type { PageLoad } from "./$types";
import type { FileListing, Listing } from "../../listing";
import type { VersionMeta } from "./meta";
export const load = (async ({ fetch, params }) => {
const result = await fetch(
`${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/${params.version}/`,
);
const data: Listing[] = await result.json();
const meta: VersionMeta | undefined = data.some(
(entry) => entry.type === "file" && entry.name === "meta.json",
)
? await fetch(
`${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/${params.version}/meta.json`,
).then((res) => res.json())
: undefined;
return {
uf2: data.find(
(entry) => entry.type === "file" && entry.name === "CURRENT.UF2",
) as FileListing,
ota: data.find(
(entry) => entry.type === "file" && entry.name === "firmware.bin",
),
version: params.version,
device: params.device,
meta: {
version: meta?.version ?? params.version,
target: meta?.target ?? params.device,
path: `${import.meta.env.VITE_FIRMWARE_URL}${params.device}/${params.version}`,
git_commit: meta?.git_commit ?? "",
git_is_dirty: meta?.git_is_dirty ?? false,
git_date: meta?.git_date ?? data[0]?.mtime ?? "",
public_build: meta?.public_build ?? !params.version.startsWith("."),
development_mode: meta?.development_mode ?? 0,
update: {
uf2:
(data.find(
(entry) =>
entry.type === "file" &&
entry.name === (meta?.update?.uf2 ?? "CURRENT.UF2"),
) as FileListing) ?? undefined,
ota:
data.find(
(entry) =>
entry.type === "file" &&
entry.name === (meta?.update?.ota ?? "firmware.bin"),
) ?? undefined,
esptool: meta?.update?.esptool ?? undefined,
},
files: data.filter(
(entry) =>
entry.type === "file" && (!meta?.files || entry.name in meta.files),
) as FileListing[],
spi_flash: meta?.spi_flash ?? undefined,
},
};
}) satisfies PageLoad;

View File

@@ -0,0 +1,41 @@
export interface VersionMeta {
version: string;
target: string;
git_commit: string;
git_is_dirty: boolean;
git_date: string;
public_build: boolean;
development_mode: number;
update: {
ota: string | null;
uf2: string | null;
esptool: EspToolData | null;
};
files: string[];
spi_flash: SPIFlashInfo | null;
}
export interface SPIFlashInfo {
type: string;
size: string;
connection: SPIConnection;
}
export interface SPIConnection {
clk: number;
q: number;
d: number;
hd: number;
cs: number;
}
export interface EspToolData {
chip: string;
baud: string;
before: string;
after: string;
flash_mode: string;
flash_freq: string;
flash_size: string;
files: Record<string, string>;
}

View File

@@ -43,7 +43,7 @@
buildIndex($chords, $osLayout).then(searchIndex.set);
});
function encodeChord(chord: ChordInfo, osLayout: Map<string, string>) {
function encodeChord(chord: ChordInfo, osLayout: Map<string, string>, onlyPhrase: boolean = false) {
const plainPhrase: string[] = [""];
const extraActions: string[] = [];
const extraCodes: string[] = [];
@@ -103,6 +103,10 @@
return result ?? `0x${it.toString(16)}`;
});
if (onlyPhrase) {
return plainPhrase.join();
}
return [
...plainPhrase,
`+${input.join("+")}`,
@@ -182,7 +186,7 @@
function downloadVocabulary() {
const vocabulary = new Set(
$chords.map((it) =>
"phrase" in it ? plainPhrase(it.phrase, $osLayout).trim() : "",
"phrase" in it ? encodeChord(it, $osLayout, true).trim() : "",
),
);
vocabulary.delete("");