mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-22 09:52:50 +00:00
Compare commits
4 Commits
df3d8a16de
...
1f4604bcbc
| Author | SHA1 | Date | |
|---|---|---|---|
|
1f4604bcbc
|
|||
|
68faf57a22
|
|||
|
1d976947e1
|
|||
|
ca8bfac3bc
|
40
.github/workflows/build.yml
vendored
40
.github/workflows/build.yml
vendored
@@ -1,13 +1,9 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on: [push]
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CI:
|
build:
|
||||||
name: 🔨🚀 Build and deploy
|
name: 🔨🚀 Build and deploy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -25,10 +21,10 @@ jobs:
|
|||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 8
|
version: 8
|
||||||
- name: 🐉 Use Node.js 18.16.x
|
- name: 🐉 Use Node.js 22.4.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18.16.x
|
node-version: 22.4.x
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- name: ⏬ Install Node dependencies
|
- name: ⏬ Install Node dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
@@ -38,17 +34,17 @@ jobs:
|
|||||||
- name: 🔨 Build site
|
- name: 🔨 Build site
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- name: 📦 Upload build artifacts
|
- name: Setup SSH
|
||||||
uses: actions/upload-artifact@v3.1.2
|
run: |
|
||||||
with:
|
install -m 600 -D /dev/null ~/.ssh/id_rsa
|
||||||
name: build
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa
|
||||||
path: build
|
echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||||
- name: Disable jekyll
|
|
||||||
run: touch build/.nojekyll
|
- name: Publish Stable
|
||||||
- name: Custom domain
|
if: ${{ github.ref == 'refs/heads/v*' }}
|
||||||
run: echo 'manager.charachorder.com' > build/CNAME
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/www/
|
||||||
- run: git config user.name github-actions
|
|
||||||
- run: git config user.email github-actions@github.com
|
- name: Publish Branch
|
||||||
- run: git --work-tree build add --all
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${GITHUB_REF##*/}
|
||||||
- run: git commit -m "Automatic Deploy action run by github-actions"
|
- name: Publish Commit
|
||||||
- run: git push origin HEAD:gh-pages --force
|
run: rsync -rav --mkpath --delete build/ deploy@charachorder.io:/home/deploy/ref/${{ github.sha }}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const config = {
|
|||||||
"update",
|
"update",
|
||||||
"offline_pin",
|
"offline_pin",
|
||||||
"warning",
|
"warning",
|
||||||
|
"dangerous",
|
||||||
|
"check",
|
||||||
"cable",
|
"cable",
|
||||||
"person",
|
"person",
|
||||||
"sync",
|
"sync",
|
||||||
|
|||||||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@@ -15,6 +15,7 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_LATEST_FIRMWARE: string;
|
readonly VITE_LATEST_FIRMWARE: string;
|
||||||
readonly VITE_STORE_URL: string;
|
readonly VITE_STORE_URL: string;
|
||||||
readonly VITE_MATRIX_URL: string;
|
readonly VITE_MATRIX_URL: string;
|
||||||
|
readonly VITE_FIRMWARE_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -440,20 +440,25 @@ export class CharaDevice {
|
|||||||
return Number(await this.send(1, "RAM").then(([bytes]) => bytes));
|
return Number(await this.send(1, "RAM").then(([bytes]) => bytes));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFirmware(file: File): Promise<void> {
|
async updateFirmware(file: File | Blob): Promise<void> {
|
||||||
const size = file.size;
|
while (this.lock) {
|
||||||
// use separate serial connection
|
await this.lock;
|
||||||
await this.port.open({ baudRate: this.baudRate });
|
}
|
||||||
const decoderStream = new TextDecoderStream();
|
let resolveLock: (result: true) => void;
|
||||||
this.port.readable!.pipeTo(decoderStream.writable);
|
this.lock = new Promise<true>((resolve) => {
|
||||||
|
resolveLock = resolve;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (this.suspendDebounceId) {
|
||||||
|
clearTimeout(this.suspendDebounceId);
|
||||||
|
} else {
|
||||||
|
await this.wake();
|
||||||
|
}
|
||||||
|
|
||||||
const reader = decoderStream
|
|
||||||
.readable!.pipeThrough(new TransformStream(new LineBreakTransformer()))
|
|
||||||
.getReader();
|
|
||||||
serialLog.update((it) => {
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "system",
|
type: "system",
|
||||||
value: "Starting firmware update",
|
value: "OTA Update",
|
||||||
});
|
});
|
||||||
return it;
|
return it;
|
||||||
});
|
});
|
||||||
@@ -461,25 +466,54 @@ export class CharaDevice {
|
|||||||
const writer = this.port.writable!.getWriter();
|
const writer = this.port.writable!.getWriter();
|
||||||
try {
|
try {
|
||||||
await writer.write(new TextEncoder().encode(`RST OTA\r\n`));
|
await writer.write(new TextEncoder().encode(`RST OTA\r\n`));
|
||||||
|
serialLog.update((it) => {
|
||||||
|
it.push({
|
||||||
|
type: "input",
|
||||||
|
value: "RST OTA",
|
||||||
|
});
|
||||||
|
return it;
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
writer.releaseLock();
|
writer.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log((await reader.read()).value);
|
// Wait for the device to be ready
|
||||||
|
const signal = await this.reader.read();
|
||||||
await file.stream().pipeTo(this.port.writable!);
|
|
||||||
|
|
||||||
console.log((await reader.read()).value);
|
|
||||||
|
|
||||||
await reader.cancel();
|
|
||||||
reader.releaseLock();
|
|
||||||
await this.port.close();
|
|
||||||
serialLog.update((it) => {
|
serialLog.update((it) => {
|
||||||
it.push({
|
it.push({
|
||||||
type: "system",
|
type: "output",
|
||||||
value: "Success?",
|
value: signal.value!.trim(),
|
||||||
});
|
});
|
||||||
return it;
|
return it;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await file.stream().pipeTo(this.port.writable!);
|
||||||
|
|
||||||
|
serialLog.update((it) => {
|
||||||
|
it.push({
|
||||||
|
type: "input",
|
||||||
|
value: `...${file.size} bytes`,
|
||||||
|
});
|
||||||
|
return it;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = (await this.reader.read()).value!.trim();
|
||||||
|
serialLog.update((it) => {
|
||||||
|
it.push({
|
||||||
|
type: "output",
|
||||||
|
value: result!,
|
||||||
|
});
|
||||||
|
return it;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.suspend();
|
||||||
|
|
||||||
|
if (result !== "OTA OK") {
|
||||||
|
throw new Error(result);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
delete this.lock;
|
||||||
|
resolveLock!(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import {
|
|
||||||
themeBase,
|
|
||||||
themeColor,
|
|
||||||
themeSuccessBase,
|
|
||||||
} from "$lib/style/theme.server";
|
|
||||||
import type { LayoutServerLoad } from "./$types";
|
|
||||||
|
|
||||||
export const load = (async () => ({
|
|
||||||
themeSuccessBase,
|
|
||||||
themeBase,
|
|
||||||
themeColor,
|
|
||||||
})) satisfies LayoutServerLoad;
|
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import type { LayoutLoad } from "./$types";
|
import type { LayoutLoad } from "./$types";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { charaFileFromUriComponent } from "$lib/share/share-url";
|
import { charaFileFromUriComponent } from "$lib/share/share-url";
|
||||||
|
import { themeBase, themeColor, themeSuccessBase } from "$lib/style/theme";
|
||||||
|
|
||||||
export const load = (async ({ url, data, fetch }) => {
|
export const load = (async ({ url, data, fetch }) => {
|
||||||
const importFile = browser && new URLSearchParams(url.search).get("import");
|
const importFile = browser && new URLSearchParams(url.search).get("import");
|
||||||
return {
|
return {
|
||||||
...data,
|
themeSuccessBase,
|
||||||
|
themeBase,
|
||||||
|
themeColor,
|
||||||
importFile: importFile
|
importFile: importFile
|
||||||
? await charaFileFromUriComponent(importFile, fetch)
|
? await charaFileFromUriComponent(importFile, fetch)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
16
src/routes/(app)/ota-update/+layout.svelte
Normal file
16
src/routes/(app)/ota-update/+layout.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1><a href="/ota-update/">Firmware Update</a></h1>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h1 {
|
||||||
|
margin-block: 1em;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 3em;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +1,91 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { serialPort } from "$lib/serial/connection";
|
import { serialPort } from "$lib/serial/connection";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
let files: FileList | null = $state(null);
|
let files: FileList | null = $state(null);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const file = files?.[0];
|
const file = files?.[0];
|
||||||
if (file && $serialPort) {
|
if (file && $serialPort) {
|
||||||
|
|
||||||
$serialPort.updateFirmware(file);
|
$serialPort.updateFirmware(file);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let currentDevice = $derived(
|
||||||
|
$serialPort
|
||||||
|
? `${$serialPort.device.toLowerCase()}_${$serialPort.chipset.toLowerCase()}`
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{#each data.devices as device}
|
||||||
|
<li>
|
||||||
|
<a href="./{device.name}/" class:highlight={device.name === currentDevice}
|
||||||
|
>{device.name}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{#if !currentDevice}
|
||||||
|
<aside transition:slide>Connect your device to see which one you need</aside>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<input type="file" accept=".bin" bind:files />
|
<input type="file" accept=".bin" bind:files />
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
outline: 1px solid var(--md-sys-color-outline);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition:
|
||||||
|
background-color 200ms ease,
|
||||||
|
color 200ms ease,
|
||||||
|
outline-offset 200ms ease,
|
||||||
|
outline-color 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes highlight {
|
||||||
|
0% {
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wiggle {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
scale: 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
outline-width: 2px;
|
||||||
|
outline-color: var(--md-sys-color-primary);
|
||||||
|
animation: wiggle 500ms ease 2 alternate;
|
||||||
|
background-color: var(--md-sys-color-primary-container);
|
||||||
|
color: var(--md-sys-color-on-primary-container);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
9
src/routes/(app)/ota-update/+page.ts
Normal file
9
src/routes/(app)/ota-update/+page.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
import type { DirectoryListing } from "./listing";
|
||||||
|
|
||||||
|
export const load = (async ({ fetch }) => {
|
||||||
|
const result = await fetch(import.meta.env.VITE_FIRMWARE_URL);
|
||||||
|
const data = await result.json();
|
||||||
|
|
||||||
|
return { devices: data as DirectoryListing[] };
|
||||||
|
}) satisfies PageLoad;
|
||||||
0
src/routes/(app)/ota-update/OTA.svelte
Normal file
0
src/routes/(app)/ota-update/OTA.svelte
Normal file
85
src/routes/(app)/ota-update/[device]/+page.svelte
Normal file
85
src/routes/(app)/ota-update/[device]/+page.svelte
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
|
let showPrerelease = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
<h2>Versions available for <em>{data.device}</em></h2>
|
||||||
|
<label
|
||||||
|
>Include Pre-releases<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={showPrerelease}
|
||||||
|
/></label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if data.versions}
|
||||||
|
<ul>
|
||||||
|
{#each data.versions as version}
|
||||||
|
{@const isPrerelease = version.name.includes("-")}
|
||||||
|
<li class:pre-release={isPrerelease}>
|
||||||
|
<a href="./{version.name}/"
|
||||||
|
>{version.name}
|
||||||
|
<time datetime={version.mtime}
|
||||||
|
>{new Date(version.mtime).toLocaleDateString()}</time
|
||||||
|
></a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<h2>The device {data.device} does not exist.</h2>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.pre-release {
|
||||||
|
margin-inline-start: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
height: 2em;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
height 200ms ease,
|
||||||
|
opacity 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-block-end: 0;
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time {
|
||||||
|
opacity: 0.5;
|
||||||
|
&:before {
|
||||||
|
content: "•";
|
||||||
|
padding-inline: 0.4ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.title:has(input:not(:checked)) ~ ul .pre-release {
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
src/routes/(app)/ota-update/[device]/+page.ts
Normal file
16
src/routes/(app)/ota-update/[device]/+page.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
import type { DirectoryListing } from "../listing";
|
||||||
|
|
||||||
|
export const load = (async ({ fetch, params }) => {
|
||||||
|
const result = await fetch(
|
||||||
|
`${import.meta.env.VITE_FIRMWARE_URL}/${params.device}/`,
|
||||||
|
);
|
||||||
|
const data = await result.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
versions: (data as DirectoryListing[]).sort((a, b) =>
|
||||||
|
b.name.localeCompare(a.name),
|
||||||
|
),
|
||||||
|
device: params.device,
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
||||||
292
src/routes/(app)/ota-update/[device]/[version]/+page.svelte
Normal file
292
src/routes/(app)/ota-update/[device]/[version]/+page.svelte
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { serialPort } from "$lib/serial/connection";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
|
let working = $state(false);
|
||||||
|
let success = $state(false);
|
||||||
|
let error = $state<Error | undefined>(undefined);
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
working = true;
|
||||||
|
error = undefined;
|
||||||
|
success = false;
|
||||||
|
const port = $serialPort!;
|
||||||
|
$serialPort = undefined;
|
||||||
|
try {
|
||||||
|
const file = await fetch(otaUrl!).then((it) => it.blob());
|
||||||
|
|
||||||
|
await port.updateFirmware(file);
|
||||||
|
|
||||||
|
success = true;
|
||||||
|
} catch (e) {
|
||||||
|
error = e as Error;
|
||||||
|
} finally {
|
||||||
|
working = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentDevice = $derived(
|
||||||
|
$serialPort
|
||||||
|
? `${$serialPort.device.toLowerCase()}_${$serialPort.chipset.toLowerCase()}`
|
||||||
|
: 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytes to respective units
|
||||||
|
*/
|
||||||
|
function toByteUnit(value: number) {
|
||||||
|
if (value < 1024) {
|
||||||
|
return `${value}B`;
|
||||||
|
} else if (value < 1024 * 1024) {
|
||||||
|
return `${(value / 1024).toFixed(2)}KB`;
|
||||||
|
} else {
|
||||||
|
return `${(value / 1024 / 1024).toFixed(2)}MB`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
Update <em
|
||||||
|
class="device"
|
||||||
|
class:correct-device={isCorrectDevice === true}
|
||||||
|
class:incorrect-device={isCorrectDevice === false}>{data.device}</em
|
||||||
|
>
|
||||||
|
to <em class="version">{data.version}</em>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<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
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>OTA Upate</h3>
|
||||||
|
{#if data.ota}
|
||||||
|
{@const buttonError = error || (!success && isCorrectDevice === false)}
|
||||||
|
<button
|
||||||
|
class:working
|
||||||
|
class:primary={!buttonError}
|
||||||
|
class:error={buttonError}
|
||||||
|
disabled={working || $serialPort === undefined || !isCorrectDevice}
|
||||||
|
onclick={update}>Apply Update</button
|
||||||
|
>
|
||||||
|
{#if $serialPort && isCorrectDevice}
|
||||||
|
<div transition:slide>
|
||||||
|
Your device is ready and compatible. Click the button to perform the
|
||||||
|
update.
|
||||||
|
</div>
|
||||||
|
{:else if $serialPort && isCorrectDevice === false}
|
||||||
|
<div class="error" transition:slide>
|
||||||
|
Your device is incompatible with the selected update.
|
||||||
|
</div>
|
||||||
|
{:else if success}
|
||||||
|
<div class="primary" transition:slide>Update successful</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="error" transition:slide>{error.message}</div>
|
||||||
|
{:else if working}
|
||||||
|
<div class="primary" transition:slide>Updating your device...</div>
|
||||||
|
{:else}
|
||||||
|
<div class="primary" transition:slide>
|
||||||
|
Connect your device to continue
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<em>There are no OTA files for this device.</em>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>Other options</h3>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h4>Via UF2</h4>
|
||||||
|
<ol>
|
||||||
|
<li>Backup your device</li>
|
||||||
|
<li>Reboot to bootloader</li>
|
||||||
|
<li>Save CURRENT.UF2 to the new drive</li>
|
||||||
|
<li>Restore</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h4>Via Serial</h4>
|
||||||
|
<p>WIP</p>
|
||||||
|
</section>
|
||||||
|
ading 0 Chordmaps.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
h2 > em {
|
||||||
|
font-style: normal;
|
||||||
|
transition: color 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(120deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: rotate(120deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
height: 42px;
|
||||||
|
|
||||||
|
border: 2px solid currentcolor;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
outline: 2px dashed currentcolor;
|
||||||
|
outline-offset: 4px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
|
transition:
|
||||||
|
border 200ms ease,
|
||||||
|
color 200ms ease;
|
||||||
|
|
||||||
|
margin: 6px;
|
||||||
|
margin-block: 16px;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.working {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.working::before {
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
height: calc(100% - 4px);
|
||||||
|
border-radius: 8px;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
&.working::after {
|
||||||
|
z-index: -2;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
animation: rotate 1s ease-out forwards infinite;
|
||||||
|
height: 30%;
|
||||||
|
width: 120%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
color: var(--md-sys-color-outline);
|
||||||
|
margin-block: 3em;
|
||||||
|
margin-inline: 5em;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
border: 1px solid var(--md-sys-color-outline);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
.size {
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding-inline-start: 0.4em;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
color: var(--md-sys-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.correct-device {
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incorrect-device {
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
src/routes/(app)/ota-update/[device]/[version]/+page.ts
Normal file
20
src/routes/(app)/ota-update/[device]/[version]/+page.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
14
src/routes/(app)/ota-update/listing.ts
Normal file
14
src/routes/(app)/ota-update/listing.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export type Listing = FileListing | DirectoryListing;
|
||||||
|
|
||||||
|
export interface DirectoryListing {
|
||||||
|
name: string;
|
||||||
|
type: "directory";
|
||||||
|
mtime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileListing {
|
||||||
|
name: string;
|
||||||
|
type: "file";
|
||||||
|
mtime: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// noinspection ES6PreferShortImport
|
// noinspection ES6PreferShortImport
|
||||||
import { themeColor } from "./src/lib/style/theme.server";
|
import { themeColor } from "./src/lib/style/theme";
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { SvelteKitPWA } from "@vite-pwa/sveltekit";
|
import { SvelteKitPWA } from "@vite-pwa/sveltekit";
|
||||||
@@ -23,6 +23,7 @@ process.env["VITE_LEARN_URL"] = "https://www.iq-eq.io/";
|
|||||||
process.env["VITE_LATEST_FIRMWARE"] = "1.1.4";
|
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/";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
@@ -54,10 +55,11 @@ export default defineConfig({
|
|||||||
workbox: {
|
workbox: {
|
||||||
// https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#globpatterns
|
// https://vite-pwa-org.netlify.app/frameworks/sveltekit.html#globpatterns
|
||||||
globPatterns: [
|
globPatterns: [
|
||||||
"client/**/*.{js,map,css,ico,woff2,csv,png,webp,svg,webmanifest}",
|
"client/**/*.{js,css,ico,woff2,csv,png,webp,svg,webmanifest}",
|
||||||
"prerendered/**/*.html",
|
"prerendered/**/*.html",
|
||||||
],
|
],
|
||||||
ignoreURLParametersMatching: [/^import|redirectUrl|loginToken$/],
|
ignoreURLParametersMatching: [/^import|redirectUrl|loginToken$/],
|
||||||
|
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||||
},
|
},
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "CharaChorder Device Manager",
|
name: "CharaChorder Device Manager",
|
||||||
|
|||||||
Reference in New Issue
Block a user