1
0
mirror of https://github.com/MultiMote/niimblue synced 2026-01-19 19:37:11 +03:00

Firmware flasher

* More compact printer menu
This commit is contained in:
MultiMote
2025-01-09 22:23:22 +03:00
parent 80ee96143a
commit f4feeab554
7 changed files with 153 additions and 19 deletions

View File

@@ -18,7 +18,7 @@
"@byteowls/capacitor-filesharer": "^6.0.0",
"@capacitor/core": "^6.1.2",
"@fontsource-variable/noto-sans": "^5.0.5",
"@mmote/niimbluelib": "0.0.1-alpha.21",
"@mmote/niimbluelib": "0.0.1-alpha.22",
"@popperjs/core": "^2.11.8",
"bootstrap": "5.3.3",
"d3-dsv": "^3.0.1",

View File

@@ -2,6 +2,7 @@ import "./style.scss";
import "@popperjs/core";
import "toastify-js/src/toastify.css";
import "bootstrap/js/dist/dropdown";
import "bootstrap/js/dist/collapse";
import App from "./App.svelte";
import { config as FabricConfig } from "fabric";

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import {
FirmwareProgressEvent,
NiimbotCapacitorBleClient,
SoundSettingsItemType,
Utils,
@@ -24,10 +25,13 @@
import { onMount } from "svelte";
import { LocalStoragePersistence } from "../utils/persistence";
import type { MaterialIcon } from "material-icons";
import { FileUtils } from "../utils/file_utils";
let connectionType: ConnectionType = "bluetooth";
let rfidInfo: RfidInfo | undefined = undefined;
let featureSupport: AvailableTransports = { webBluetooth: false, webSerial: false, capacitorBle: false };
let fwVersion: string = "";
let fwProgress: string = "";
const onConnectClicked = async () => {
initClient(connectionType);
@@ -76,6 +80,34 @@
await $printerClient.abstraction.printerReset();
};
const upgradeFw = async () => {
if (!confirm("Flashing wrong firmware can make your printer dead. Are you sure?")) {
return;
}
const data = await FileUtils.pickAndReadBinaryFile("bin");
const listener = (e: FirmwareProgressEvent) => {
fwProgress = `${e.currentChunk}/${e.totalChunks}`;
};
$printerClient.stopHeartbeat();
try {
$printerClient.on("firmwareprogress", listener);
fwProgress = "...";
await $printerClient.abstraction.firmwareUpgrade(data, fwVersion);
$printerClient.off("firmwareprogress", listener);
await $printerClient.disconnect();
Toasts.message("Flashing is finished, the printer will shut down now");
} catch (e) {
Toasts.error(e);
$printerClient.startHeartbeat();
$printerClient.off("firmwareprogress", listener);
}
};
const switchConnectionType = (c: ConnectionType) => {
LocalStoragePersistence.saveLastConnectionType(c);
connectionType = c;
@@ -133,8 +165,15 @@
{/if}
{#if $printerMeta}
<div>
Model metadata:
<button
class="btn btn-sm btn-outline-secondary d-block w-100 mt-1"
type="button"
data-bs-toggle="collapse"
data-bs-target="#modelMeta">
Model metadata <MdIcon icon="expand_more" />
</button>
<div class="collapse" id="modelMeta">
<ul>
{#each Object.entries($printerMeta) as [k, v]}
<li>{k}: <strong>{v ?? "-"}</strong></li>
@@ -144,8 +183,15 @@
{/if}
{#if rfidInfo}
<div>
Rfid info:
<button
class="btn btn-sm btn-outline-secondary d-block w-100 mt-1"
type="button"
data-bs-toggle="collapse"
data-bs-target="#rfidInfo">
Rfid info <MdIcon icon="expand_more" />
</button>
<div class="collapse" id="rfidInfo">
<ul>
{#each Object.entries(rfidInfo) as [k, v]}
<li>{k}: <strong>{v ?? "-"}</strong></li>
@@ -155,8 +201,15 @@
{/if}
{#if $heartbeatData}
<div>
Heartbeat data:
<button
class="btn btn-sm btn-outline-secondary d-block w-100 mt-1"
type="button"
data-bs-toggle="collapse"
data-bs-target="#heartbeatData">
Heartbeat data <MdIcon icon="expand_more" />
</button>
<div class="collapse" id="heartbeatData">
<ul>
{#each Object.entries($heartbeatData) as [k, v]}
<li>{k}: <strong>{v ?? "-"}</strong></li>
@@ -165,15 +218,35 @@
</div>
{/if}
<div>Tests</div>
<div class="input-group input-group-sm mt-1">
{#if fwProgress}
<span class="input-group-text">Uploading {fwProgress}</span>
{:else}
<button class="btn btn-sm btn-primary" on:click={upgradeFw} disabled={!!fwProgress}>Upgrade FW</button>
<span class="input-group-text">To version</span>
<input class="form-control" placeholder="x.x" type="text" size="6" bind:value={fwVersion} />
{/if}
</div>
<button class="btn btn-sm btn-primary" on:click={getRfidInfo}>Rfid</button>
<button class="btn btn-sm btn-primary" on:click={startHeartbeat}>Heartbeat on</button>
<button class="btn btn-sm btn-primary" on:click={stopHeartbeat}>Heartbeat off</button>
<button class="btn btn-sm btn-primary" on:click={soundOn}>Sound on</button>
<button class="btn btn-sm btn-primary" on:click={soundOff}>Sound off</button>
<button class="btn btn-sm btn-primary" on:click={fetchInfo}>Fetch info again</button>
<button class="btn btn-sm btn-primary" on:click={reset}>Reset</button>
<button
class="btn btn-sm btn-outline-secondary d-block w-100 mt-1"
type="button"
data-bs-toggle="collapse"
data-bs-target="#tests">
Tests <MdIcon icon="expand_more" />
</button>
<div class="collapse" id="tests">
<div class="d-flex flex-wrap gap-1 mt-1">
<button class="btn btn-sm btn-primary" on:click={getRfidInfo}>Rfid</button>
<button class="btn btn-sm btn-primary" on:click={startHeartbeat}>Heartbeat on</button>
<button class="btn btn-sm btn-primary" on:click={stopHeartbeat}>Heartbeat off</button>
<button class="btn btn-sm btn-primary" on:click={soundOn}>Sound on</button>
<button class="btn btn-sm btn-primary" on:click={soundOff}>Sound off</button>
<button class="btn btn-sm btn-primary" on:click={fetchInfo}>Fetch info again</button>
<button class="btn btn-sm btn-primary" on:click={reset}>Reset</button>
</div>
</div>
</div>
<span class="input-group-text {$heartbeatFails > 0 ? 'text-warning' : ''}">
{$printerMeta?.model ?? $connectedPrinterName}

View File

@@ -11,6 +11,7 @@
@import "bootstrap/scss/close";
@import "bootstrap/scss/progress";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/transitions";
@import "font";
body {
@@ -22,3 +23,9 @@ body {
border: 1px solid var(--bs-danger-border-subtle);
color: var(--bs-danger-text-emphasis);
}
.toastify.toast-info {
background: var(--bs-success-bg-subtle);
border: 1px solid var(--bs-success-border-subtle);
color: var(--bs-success-text-emphasis);
}

View File

@@ -137,6 +137,44 @@ export class FileUtils {
});
}
/**
* Open file picker and return file contents
*
* fixme: never ends if dialog closed
*
* */
static async pickAndReadBinaryFile(acceptExtension: string): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const input: HTMLInputElement = document.createElement("input");
const reader = new FileReader();
input.type = "file";
input.accept = `.${acceptExtension}`;
input.onchange = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files !== null) {
const file: File = target.files[0];
const ext = file.name.split(".").pop();
if (ext === acceptExtension) {
reader.readAsArrayBuffer(file);
reader.onload = (readerEvt: ProgressEvent<FileReader>) => {
if (readerEvt?.target?.result) {
resolve(new Uint8Array(readerEvt.target.result as ArrayBuffer));
}
};
reader.onerror = (readerEvt: ProgressEvent<FileReader>) => {
console.error(readerEvt);
reject(new Error("Unable to load file"));
};
}
}
};
input.click();
});
}
static async loadCanvasState(canvas: fabric.Canvas, state: FabricJson): Promise<void> {
await canvas.loadFromJSON(state, (_, obj) => {
if ("set" in obj) obj.set({ snapAngle: OBJECT_DEFAULTS.snapAngle });

View File

@@ -13,6 +13,15 @@ export class Toasts {
}).showToast();
}
static message(text: string) {
Toastify({
text,
gravity: "bottom",
duration: 5000,
className: "toast-info",
}).showToast();
}
static zodErrors(e: any, prefix: string) {
if (e instanceof z.ZodError) {
e.issues.forEach((i) => {

View File

@@ -183,14 +183,15 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@mmote/niimbluelib@0.0.1-alpha.21":
version "0.0.1-alpha.21"
resolved "https://registry.yarnpkg.com/@mmote/niimbluelib/-/niimbluelib-0.0.1-alpha.21.tgz#35b2c31094070c69aec4663521ce64475662ebfb"
integrity sha512-wSJ1d7VgLwl6G71DOmKITX7XX1OypZaPNim+Aa2Dpa6nyifo1v8WrYOr4aKGGJzZt1oSxQ+spWv2thsQrX2Baw==
"@mmote/niimbluelib@0.0.1-alpha.22":
version "0.0.1-alpha.22"
resolved "https://registry.yarnpkg.com/@mmote/niimbluelib/-/niimbluelib-0.0.1-alpha.22.tgz#34ee209616dcf3988a063eb154d83251315928d6"
integrity sha512-XsD9DyyD7RaNlZwqZinLopFy6knjov1JU4nnrA9iqJ4Tdv5sDu6WFHdUKR8Ip8+fkJ7sBF6UHi7y+Ucc4A5wIg==
dependencies:
"@capacitor-community/bluetooth-le" "^6.0.2"
"@capacitor/core" "^6.0.0"
async-mutex "^0.5.0"
crc-32 "^1.2.2"
eventemitter3 "^5.0.1"
"@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
@@ -507,6 +508,11 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
crc-32@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
css-tree@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"