webapp: implemented firmware upgrade UI
This commit is contained in:
parent
da82f1fbbd
commit
c4bd3c196f
@ -12,6 +12,7 @@
|
|||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"bootstrap-icons-vue": "^1.8.1",
|
"bootstrap-icons-vue": "^1.8.1",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"spark-md5": "^3.0.2",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-router": "^4.0.14"
|
"vue-router": "^4.0.14"
|
||||||
},
|
},
|
||||||
|
|||||||
184
webapp/src/components/FirmwareUpgradeView.vue
Normal file
184
webapp/src/components/FirmwareUpgradeView.vue
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container" role="main">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Firmware Upgrade</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="position-relative" v-if="loading">
|
||||||
|
<div class="position-absolute top-50 start-50 translate-middle">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && !uploading && OTAError !== null" class="card">
|
||||||
|
<div class="card-header text-white bg-danger">OTA Error</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<p class="h1 mb-2"><BIconExclamationCircleFill /></p>
|
||||||
|
|
||||||
|
<span style="vertical-align: middle" class="ml-2">
|
||||||
|
{{ OTAError }}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button class="btn btn-light" @click="clear">
|
||||||
|
<BIconArrowLeft /> Back
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" @click="retryOTA">
|
||||||
|
<BIconArrowRepeat /> Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!loading && !uploading && OTASuccess" class="card">
|
||||||
|
<div class="card-header text-white bg-success">OTA Status</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="h1 mb-2"><BIconCheckCircle /></span>
|
||||||
|
<span> OTA Success </span>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button class="btn btn-primary" @click="clear">
|
||||||
|
<BIconArrowLeft /> Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!loading && !uploading" class="card">
|
||||||
|
<div class="card-header text-white bg-primary">Firmware Upload</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="form-group pt-2 mt-3">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="file"
|
||||||
|
ref="file"
|
||||||
|
accept=".bin,.bin.gz"
|
||||||
|
@change="uploadOTA"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!loading && uploading" class="card">
|
||||||
|
<div class="card-header text-white bg-primary">Upload Progress</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="progress">
|
||||||
|
<div
|
||||||
|
class="progress-bar"
|
||||||
|
role="progressbar"
|
||||||
|
:style="{ width: this.progress + '%' }"
|
||||||
|
v-bind:aria-valuenow="this.progress"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
>
|
||||||
|
{{ progress }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SparkMD5 from "spark-md5";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
uploading: false,
|
||||||
|
progress: 0,
|
||||||
|
OTAError: null,
|
||||||
|
OTASuccess: false,
|
||||||
|
type: "firmware",
|
||||||
|
file: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fileMD5(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const blobSlice =
|
||||||
|
File.prototype.slice ||
|
||||||
|
File.prototype.mozSlice ||
|
||||||
|
File.prototype.webkitSlice;
|
||||||
|
const chunkSize = 2097152; // Read in chunks of 2MB
|
||||||
|
const chunks = Math.ceil(file.size / chunkSize);
|
||||||
|
const spark = new SparkMD5.ArrayBuffer();
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
let currentChunk = 0;
|
||||||
|
let loadNext;
|
||||||
|
fileReader.onload = (e) => {
|
||||||
|
spark.append(e.target.result); // Append array buffer
|
||||||
|
currentChunk += 1;
|
||||||
|
if (currentChunk < chunks) {
|
||||||
|
loadNext();
|
||||||
|
} else {
|
||||||
|
const md5 = spark.end();
|
||||||
|
resolve(md5);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fileReader.onerror = (e) => {
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
loadNext = () => {
|
||||||
|
const start = currentChunk * chunkSize;
|
||||||
|
const end =
|
||||||
|
start + chunkSize >= file.size ? file.size : start + chunkSize;
|
||||||
|
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
|
||||||
|
};
|
||||||
|
loadNext();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
uploadOTA(event) {
|
||||||
|
this.uploading = true;
|
||||||
|
const formData = new FormData();
|
||||||
|
if (event !== null) {
|
||||||
|
[this.file] = event.target.files;
|
||||||
|
}
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
request.addEventListener("load", () => {
|
||||||
|
// request.response will hold the response from the server
|
||||||
|
if (request.status === 200) {
|
||||||
|
this.OTASuccess = true;
|
||||||
|
} else if (request.status !== 500) {
|
||||||
|
this.OTAError = `[HTTP ERROR] ${request.statusText}`;
|
||||||
|
} else {
|
||||||
|
this.OTAError = request.responseText;
|
||||||
|
}
|
||||||
|
this.uploading = false;
|
||||||
|
this.progress = 0;
|
||||||
|
});
|
||||||
|
// Upload progress
|
||||||
|
request.upload.addEventListener("progress", (e) => {
|
||||||
|
this.progress = Math.trunc((e.loaded / e.total) * 100);
|
||||||
|
});
|
||||||
|
request.withCredentials = true;
|
||||||
|
this.fileMD5(this.file)
|
||||||
|
.then((md5) => {
|
||||||
|
formData.append("MD5", md5);
|
||||||
|
formData.append("firmware", this.file, "firmware");
|
||||||
|
request.open("post", "/api/firmware/update");
|
||||||
|
request.send(formData);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.OTAError =
|
||||||
|
"Unknown error while upload, check the console for details.";
|
||||||
|
this.uploading = false;
|
||||||
|
this.progress = 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
retryOTA() {
|
||||||
|
this.OTAError = null;
|
||||||
|
this.OTASuccess = false;
|
||||||
|
this.uploadOTA(null);
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
this.OTAError = null;
|
||||||
|
this.OTASuccess = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -50,6 +50,12 @@
|
|||||||
>Inverter Settings</router-link
|
>Inverter Settings</router-link
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
<li>
|
||||||
|
<router-link class="dropdown-item" to="/firmware/upgrade"
|
||||||
|
>Firmware Upgrade</router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import NtpAdminView from '@/components/NtpAdminView'
|
|||||||
import MqttAdminView from '@/components/MqttAdminView'
|
import MqttAdminView from '@/components/MqttAdminView'
|
||||||
import MqttInfoView from '@/components/MqttInfoView'
|
import MqttInfoView from '@/components/MqttInfoView'
|
||||||
import InverterAdminView from '@/components/InverterAdminView'
|
import InverterAdminView from '@/components/InverterAdminView'
|
||||||
|
import FirmwareUpgradeView from '@/components/FirmwareUpgradeView'
|
||||||
|
|
||||||
const routes = [{
|
const routes = [{
|
||||||
path: '/',
|
path: '/',
|
||||||
@ -59,6 +60,11 @@ const routes = [{
|
|||||||
path: '/settings/inverter',
|
path: '/settings/inverter',
|
||||||
name: 'Inverter Settings',
|
name: 'Inverter Settings',
|
||||||
component: InverterAdminView
|
component: InverterAdminView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/firmware/upgrade',
|
||||||
|
name: 'Firmware Upgrade',
|
||||||
|
component: FirmwareUpgradeView
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@ -5452,6 +5452,11 @@ sourcemap-codec@^1.4.8:
|
|||||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||||
|
|
||||||
|
spark-md5@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
|
||||||
|
integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
|
||||||
|
|
||||||
spdx-correct@^3.0.0:
|
spdx-correct@^3.0.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user