25 Commits

Author SHA1 Message Date
a3bf9ac32b 2.2.3 2025-01-15 11:31:47 +01:00
David Villafaña
5bd3245084 fix: typographical error (#156)
Co-authored-by: David Rog Desktop <dvillafanaiv@proton.me>
2025-01-15 11:30:46 +01:00
1cd2ec318a 2.2.2 2025-01-14 13:35:53 +01:00
6c8bfa0272 fix: ota update 2025-01-14 13:31:22 +01:00
f69be14b5e 2.2.1 2025-01-06 19:34:28 +01:00
dce554fc66 fix: set pnpm version in github actions correctly 2025-01-06 19:32:43 +01:00
f152dbdcf5 fix: set node/pnpm versions correctly 2025-01-06 19:31:04 +01:00
6a29e6a2fc 2.2.0 2025-01-06 19:25:45 +01:00
9bf3801fef Mark factory flash as wip 2025-01-06 19:25:27 +01:00
d2accfb838 Squash merge fix-vocabulary-export into master 2024-12-09 18:41:26 +01:00
b8a376b93b feat: update m4g 2024-12-09 18:35:05 +01:00
588719df91 feat: support factory flashing 2024-11-23 19:02:35 +01:00
6a0dad9dad feat: android support 2024-11-23 15:07:35 +01:00
f3704e4051 2.1.0 2024-11-20 22:26:59 +01:00
3e6298717e feat: m4gr 2024-11-19 22:25:01 +01:00
aced0bbbb7 feat: m4g support 2024-11-19 17:48:50 +01:00
Raymond Li
36874c59e3 Temporarily make chat available 2024-11-19 06:08:37 +00:00
9dc61a3482 fix: exclude pre-rendered ccos update pages 2024-11-08 16:04:50 +01:00
d9183f952a 2.0.2 2024-11-08 15:48:26 +01:00
913a833824 fix: build 2024-11-08 15:47:20 +01:00
0d6ef4d011 2.0.1 2024-11-08 15:43:23 +01:00
232045964c fix: firmware updates 2024-11-08 15:42:58 +01:00
3659b80e41 fix: firmware cannot be linked 2024-11-08 15:21:53 +01:00
3a02caeb6d fix: pre-production devices are not recognized by the device manager 2024-11-07 21:53:59 +01:00
259fd3a989 fix: stable pipeline 2024-11-05 02:51:21 +01:00
27 changed files with 394 additions and 106 deletions

View File

@@ -20,7 +20,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 8 version: 9
- name: 🐉 Use Node.js 22.4.x - name: 🐉 Use Node.js 22.4.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
@@ -41,7 +41,7 @@ jobs:
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Publish Stable - name: Publish Stable
if: ${{ github.ref == 'refs/heads/v*' }} if: ${{ github.ref == 'refs/tags/v*' }}
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/ run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/
- name: Publish Branch - name: Publish Branch

View File

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

View File

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

30
pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,37 @@
name: M4G
col:
# Ring / Middle
- offset: [2, 0]
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

@@ -13,7 +13,7 @@
"GTM stands for Generative Text Menu and can be used to change your device's settings anywhere", "GTM stands for Generative Text Menu and can be used to change your device's settings anywhere",
"Ambidextrous Throwover (aka Mirror Mode) is a mode designed for one-handed use", "Ambidextrous Throwover (aka Mirror Mode) is a mode designed for one-handed use",
"Chentry stands for character entry, or typing letter by letter on a chording enabled device", "Chentry stands for character entry, or typing letter by letter on a chording enabled device",
"Chord modifiers are hard-coded (as of now) and can be used in the English language to add conjucations and more", "Chord modifiers are hard-coded (as of now) and can be used in the English language to add conjugations and more",
"You can use 'cursor warping' by adding arrow key actions to a chord, for example to chord '()' with the cursor in the middle of the brackets", "You can use 'cursor warping' by adding arrow key actions to a chord, for example to chord '()' with the cursor in the middle of the brackets",
"An arpeggiate is a single key press that modifies the preceding chord. Modifiers can be arpeggiated", "An arpeggiate is a single key press that modifies the preceding chord. Modifiers can be arpeggiated",
"Some actions are marked as a 'macro', which means the output is generated by a key sequence rather than a pure key press. Be cautious with those, as they can affect other keys when held together!", "Some actions are marked as a 'macro', which means the output is generated by a key sequence rather than a pure key press. Be cautious with those, as they can affect other keys when held together!",

View File

@@ -37,6 +37,10 @@
import("$lib/assets/layouts/m4g.yml").then( import("$lib/assets/layouts/m4g.yml").then(
(it) => it.default as VisualLayout, (it) => it.default as VisualLayout,
), ),
M4GR: () =>
import("$lib/assets/layouts/m4gr.yml").then(
(it) => it.default as VisualLayout,
),
}; };
</script> </script>
@@ -70,6 +74,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 20cm;
} }
fieldset { fieldset {

View File

@@ -12,7 +12,7 @@ import { browser } from "$app/environment";
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([ const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }], ["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
["TWO S3", { usbProductId: 0x8252, usbVendorId: 0x303a }], // TODO: remove this after everyone has migrated ["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }], // TODO: remove this after everyone has migrated
["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }], ["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }],
["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }], ["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }],
["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }], ["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }],
@@ -26,6 +26,7 @@ const KEY_COUNTS = {
LITE: 67, LITE: 67,
X: 256, X: 256,
M4G: 90, M4G: 90,
M4GR: 90,
} as const; } as const;
if ( if (
@@ -36,6 +37,13 @@ if (
await import("./tauri-serial"); 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[]> { export async function getViablePorts(): Promise<SerialPort[]> {
return navigator.serial.getPorts().then((ports) => return navigator.serial.getPorts().then((ports) =>
ports.filter((it) => { ports.filter((it) => {
@@ -513,11 +521,11 @@ export class CharaDevice {
const writer2 = this.port.writable!.getWriter(); const writer2 = this.port.writable!.getWriter();
try { try {
await writer2.write(new TextEncoder().encode(`RST REBOOT\r\n`)); await writer2.write(new TextEncoder().encode(`RST RESTART\r\n`));
serialLog.update((it) => { serialLog.update((it) => {
it.push({ it.push({
type: "input", type: "input",
value: "RST REBOOT", value: "RST RESTART",
}); });
return it; return it;
}); });

View File

@@ -82,7 +82,7 @@
</li> </li>
<li> <li>
<a <a
href="/firmware/{currentDevice ? `${currentDevice}/` : ''}" href="/ccos/{currentDevice ? `${currentDevice}/` : ''}"
use:action={{ title: "Updates" }} use:action={{ title: "Updates" }}
> >
CCOS {$serialPort?.version ?? "Updates"} CCOS {$serialPort?.version ?? "Updates"}

View File

@@ -27,9 +27,9 @@
external: true, external: true,
}, },
{ href: "/editor", icon: "edit_document", title: "Editor", wip: true }, { href: "/editor", icon: "edit_document", title: "Editor", wip: true },
{ href: "https://chat.dev.charachorder.io", icon: "chat", title: "Chat", wip: true },
], ],
/*[ /*[
{ href: "/chat", icon: "chat", title: "Chat", wip: true },
{ href: "/plugin", icon: "code", title: "Plugin", wip: true }, { href: "/plugin", icon: "code", title: "Plugin", wip: true },
],*/ ],*/
] satisfies { ] satisfies {

View File

@@ -2,7 +2,7 @@
let { children } = $props(); let { children } = $props();
</script> </script>
<h1><a href="/firmware">Firmware Updates</a></h1> <h1><a href="/ccos">Firmware Updates</a></h1>
{@render children()} {@render children()}

View File

@@ -2,6 +2,7 @@
import { downloadBackup } from "$lib/backup/backup"; import { downloadBackup } from "$lib/backup/backup";
import { initSerial, serialPort } from "$lib/serial/connection"; import { initSerial, serialPort } from "$lib/serial/connection";
import { fade, slide } from "svelte/transition"; import { fade, slide } from "svelte/transition";
import type { LoaderOptions, ESPLoader } from "esptool-js";
let { data } = $props(); let { data } = $props();
@@ -9,7 +10,12 @@
let success = $state(false); let success = $state(false);
let error = $state<Error | undefined>(undefined); let error = $state<Error | undefined>(undefined);
let terminalOutput = $state("");
let step = $state(0); let step = $state(0);
let eraseAll = $state(false);
let espLoader;
async function update() { async function update() {
working = true; working = true;
@@ -18,7 +24,9 @@
const port = $serialPort!; const port = $serialPort!;
$serialPort = undefined; $serialPort = undefined;
try { try {
const file = await fetch(otaUrl!).then((it) => it.blob()); const file = await fetch(
`${data.meta.path}/${data.meta.update.ota?.name}`,
).then((it) => it.blob());
await port.updateFirmware(file); await port.updateFirmware(file);
@@ -36,18 +44,7 @@
: undefined, : undefined,
); );
let isCorrectDevice = $derived( let isCorrectDevice = $derived(
currentDevice ? currentDevice === data.device : undefined, currentDevice ? currentDevice === data.meta.target : 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,
); );
/** /**
@@ -84,10 +81,12 @@
} }
async function getFileSystem() { async function getFileSystem() {
if (!uf2Url) return; if (!data.meta.update.uf2) return;
const uf2Promise = fetch(uf2Url).then((it) => it.blob()); const uf2Promise = fetch(
`${data.meta.path}/${data.meta.update.uf2.name}`,
).then((it) => it.blob());
const handle = await window.showSaveFilePicker({ const handle = await window.showSaveFilePicker({
id: `${data.device}-update`, id: `${data.meta.target}-update`,
suggestedName: "CURRENT.UF2", suggestedName: "CURRENT.UF2",
excludeAcceptAllOption: true, excludeAcceptAllOption: true,
types: [ types: [
@@ -102,19 +101,104 @@
await uf2.stream().pipeTo(writable); await uf2.stream().pipeTo(writable);
step = 4; 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> </script>
<div> <div class="container">
<h2> <h2>
Update <em <a class="inline-link" href="/ccos">CCOS</a> /
class="device" <a
href="/ccos/{data.meta.target}"
class="device inline-link"
class:correct-device={isCorrectDevice === true} class:correct-device={isCorrectDevice === true}
class:incorrect-device={isCorrectDevice === false}>{data.device}</em class:incorrect-device={isCorrectDevice === false}>{data.meta.target}</a
> >
to <em class="version">{data.version}</em> / <em class="version">{data.meta.version}</em>
</h2> </h2>
{#if data.ota && !data.device.endsWith("m0")} {#if data.meta.update.ota && !data.meta.target.endsWith("m0")}
{@const buttonError = error || (!success && isCorrectDevice === false)} {@const buttonError = error || (!success && isCorrectDevice === false)}
<section> <section>
<button <button
@@ -127,8 +211,14 @@
> >
{#if $serialPort && isCorrectDevice} {#if $serialPort && isCorrectDevice}
<div transition:slide> <div transition:slide>
Your device is ready and compatible. Click the button to perform the Your
update. <b
>{$serialPort.company}
{$serialPort.device}
{$serialPort.chipset}</b
>
will be updated from <b class="version">{$serialPort.version}</b> to
<b class="version">{data.meta.version}</b>
</div> </div>
{:else if $serialPort && isCorrectDevice === false} {:else if $serialPort && isCorrectDevice === false}
<div class="error" transition:slide> <div class="error" transition:slide>
@@ -147,32 +237,9 @@
{/if} {/if}
</section> </section>
<hr />
<h3>Manual Update</h3> <h3>Manual Update</h3>
{/if} {/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} {#if isCorrectDevice === false}
<div transition:slide class="incorrect-device"> <div transition:slide class="incorrect-device">
These files are incompatible with your device These files are incompatible with your device
@@ -180,7 +247,6 @@
{/if} {/if}
<section> <section>
<h4>UF2 Instructions</h4>
<ol> <ol>
<li> <li>
<button class="inline-button" onclick={connect} <button class="inline-button" onclick={connect}
@@ -221,6 +287,37 @@
</li> </li>
</ol> </ol>
</section> </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> </div>
<style lang="scss"> <style lang="scss">
@@ -229,6 +326,14 @@
transition: color 200ms ease; transition: color 200ms ease;
} }
h3 {
margin-block-start: 4em;
}
pre {
overflow: auto;
}
.primary { .primary {
color: var(--md-sys-color-primary); color: var(--md-sys-color-primary);
} }
@@ -237,6 +342,11 @@
color: var(--md-sys-color-error); color: var(--md-sys-color-error);
} }
.container {
width: calc(min(100%, 16cm));
overflow: auto;
}
@keyframes rotate { @keyframes rotate {
0% { 0% {
transform: rotate(120deg); transform: rotate(120deg);
@@ -375,6 +485,11 @@
opacity: 0.6; opacity: 0.6;
} }
.inline-link {
display: inline;
padding: 0;
}
.correct-device { .correct-device {
color: var(--md-sys-color-primary); color: var(--md-sys-color-primary);
opacity: 1; opacity: 1;
@@ -383,4 +498,8 @@
.incorrect-device { .incorrect-device {
color: var(--md-sys-color-error); color: var(--md-sys-color-error);
} }
.esp-buttons {
display: flex;
}
</style> </style>

View File

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

View File

@@ -1,12 +1,13 @@
import type { Action } from "svelte/action"; import type { Action } from "svelte/action";
import ConfirmChallenge from "./ConfirmChallenge.svelte"; import ConfirmChallenge from "./ConfirmChallenge.svelte";
import tippy from "tippy.js"; import tippy from "tippy.js";
import { mount, unmount } from "svelte";
export const confirmChallenge: Action< export const confirmChallenge: Action<
HTMLElement, HTMLElement,
{ onConfirm: () => void; challenge: string } { onConfirm: () => void; challenge: string }
> = (node, { onConfirm, challenge }) => { > = (node, { onConfirm, challenge }) => {
let component: ConfirmChallenge | undefined; let component: {} | undefined;
let target: HTMLElement | undefined; let target: HTMLElement | undefined;
const edit = tippy(node, { const edit = tippy(node, {
interactive: true, interactive: true,
@@ -15,15 +16,22 @@ export const confirmChallenge: Action<
target = instance.popper.querySelector(".tippy-content") as HTMLElement; target = instance.popper.querySelector(".tippy-content") as HTMLElement;
target.classList.add("active"); target.classList.add("active");
if (component === undefined) { if (component === undefined) {
component = new ConfirmChallenge({ target, props: { challenge } }); component = mount(ConfirmChallenge, {
component.$on("confirm", () => { target,
edit.hide(); props: {
onConfirm(); challenge,
onconfirm() {
edit.hide();
onConfirm();
},
},
}); });
} }
}, },
onHidden() { onHidden() {
component?.$destroy(); if (component) {
unmount(component);
}
target?.classList.remove("active"); target?.classList.remove("active");
component = undefined; component = undefined;
}, },

View File

@@ -1,20 +0,0 @@
import type { PageLoad } from "./$types";
import type { FileListing, Listing } from "../../listing";
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();
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,
};
}) satisfies PageLoad;

View File

@@ -58,6 +58,7 @@ export default defineConfig({
"client/**/*.{js,css,ico,woff2,csv,png,webp,svg,webmanifest}", "client/**/*.{js,css,ico,woff2,csv,png,webp,svg,webmanifest}",
"prerendered/**/*.html", "prerendered/**/*.html",
], ],
globIgnores: ["prerendered/pages/ccos/**/*"],
ignoreURLParametersMatching: [/^import|redirectUrl|loginToken$/], ignoreURLParametersMatching: [/^import|redirectUrl|loginToken$/],
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
}, },