App.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <template>
  2. <!-- eslint-disable max-len -->
  3. <main class="container">
  4. <div class="columns mt-3">
  5. <div class="col-12 mt-3 text-center">
  6. <img width="280px" alt="ElegantOTA" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDI1NCA4MyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMjU0IDgzOyI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6IzJFMzAzNDt9Cgkuc3Qxe2ZpbGw6IzQ4OEVGRjt9Cjwvc3R5bGU+CjxnIGlkPSJfeDMwX2I3NDI0NjctMjdlYS1hZTNmLTdjZmEtY2EwZTEwYWMwNGU3IiB0cmFuc2Zvcm09Im1hdHJpeCgyLjIsMCwwLDIuMiwxMDMuODg3NjQxNjY4MzE5NywxMzIuMzU1OTk2MDM2NTI5NTUpIj4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0tNi4xLTQ1LjR2LTFILTEzdjkuM2g2Ljh2LTFoLTUuOHYtMy4xaDUuMnYtMWgtNS4ydi0zLjFILTYuMXogTS0zLjYtNDdoLTEuMXY5LjhoMS4xVi00N3ogTTEuMy0zOCAgIGMtMS40LDAtMi4zLTEtMi41LTIuMmg1Ljl2LTAuNGMwLTItMS4zLTMuNi0zLjUtMy42cy0zLjUsMS42LTMuNSwzLjZTLTAuOC0zNywxLjMtMzdDMy0zNyw0LTM3LjksNC41LTM5LjJIMy4zICAgQzMtMzguNSwyLjMtMzgsMS4zLTM4eiBNMS4yLTQzLjNjMS4zLDAsMi4yLDAuOSwyLjQsMi4xaC00LjhDLTAuOS00Mi40LTAuMS00My4zLDEuMi00My4zeiBNMTEuNS00NC4ydjEuMSAgIGMtMC41LTAuOC0xLjMtMS4yLTIuNS0xLjJjLTIsMC0zLjQsMS42LTMuNCwzLjZjMCwyLDEuMywzLjYsMy40LDMuNmMxLjIsMCwyLTAuNSwyLjUtMS4zdjFjMCwxLjQtMC43LDIuMi0yLjMsMi4yICAgYy0xLjEsMC0xLjgtMC40LTItMS4xSDUuOWMwLjQsMS4yLDEuNSwyLDMuMywyYzIuMSwwLDMuMy0xLjEsMy4zLTMuMXYtNi44SDExLjV6IE05LjEtMzguMWMtMS42LDAtMi41LTEuMi0yLjUtMi42ICAgYzAtMS40LDAuOS0yLjYsMi41LTIuNmMxLjUsMCwyLjQsMS4yLDIuNCwyLjZDMTEuNS0zOS4zLDEwLjYtMzguMSw5LjEtMzguMXogTTE3LjMtNDQuM2MtMiwwLTMuNCwxLjYtMy40LDMuNnMxLjMsMy42LDMuNCwzLjYgICBjMS4yLDAsMi0wLjUsMi41LTEuMnYxLjFoMXYtN2gtMXYxLjFDMTkuMy00My44LDE4LjQtNDQuMywxNy4zLTQ0LjN6IE0xNy40LTM4Yy0xLjYsMC0yLjUtMS4yLTIuNS0yLjdjMC0xLjQsMC45LTIuNywyLjUtMi43ICAgYzEuNiwwLDIuNCwxLjIsMi40LDIuN0MxOS44LTM5LjIsMTktMzgsMTcuNC0zOHogTTIyLjctNDQuMnY3aDF2LTRjMC0xLjMsMS0yLjEsMi4xLTIuMWMxLjIsMCwyLDAuOCwyLDIuMXY0aDF2LTQuMSAgIGMwLTItMS4zLTMtMi44LTNjLTEuMSwwLTEuOCwwLjUtMi4zLDEuMnYtMS4xSDIyLjd6IE0zMi4yLTM3LjJ2LTYuMWgxLjZ2LTAuOWgtMS42di0yLjVoLTEuMXYyLjVoLTEuNHYwLjloMS40djYuMUgzMi4yeiAgICBNMzkuMy00Ni41Yy0yLjgsMC00LjcsMi4xLTQuNyw0LjhjMCwyLjYsMS45LDQuNyw0LjcsNC43czQuNy0yLjEsNC43LTQuN0M0NC00NC40LDQyLjEtNDYuNSwzOS4zLTQ2LjV6IE0zOS4zLTM4LjEgICBjLTIuMywwLTMuNy0xLjYtMy43LTMuN2MwLTIuMSwxLjQtMy43LDMuNy0zLjdzMy43LDEuNiwzLjcsMy43QzQzLTM5LjcsNDEuNi0zOC4xLDM5LjMtMzguMXogTTUxLjktNDYuNGgtNy41djFoMy4ydjguM2gxdi04LjMgICBoMy4yVi00Ni40eiBNNTYuNS00Ni40aC0xLjNsLTMuOSw5LjNoMS4ybDEuMS0yLjdoNC42bDEuMSwyLjdoMS4yTDU2LjUtNDYuNHogTTU0LTQwLjhsMS45LTQuNWwxLjksNC41SDU0eiIvPgo8L2c+CjxnIGlkPSJkYzY4OGQxZi1hMDY2LWI3ZTctOGFiYy0xMTVmYTQwZTk0NDUiIHRyYW5zZm9ybT0ibWF0cml4KDAuMjI1NTIwNTc0ODgyOTcxMTEsMCwwLDAuMjI1NTIwNTc0ODgyOTcxMTEsMzQuNTIyNzQzOTgxNjY2ODU2LDExNy4wMTc3NDQ5NjcyNzM1KSI+Cgk8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTE5LTM0Ny42YzUuNywwLDcuOCw2LjYsNC41LDEwLjhjLTcuMiw4LjQtMTQuNCwxNi41LTIxLjYsMjQuOWMtMi4xLDIuNC02LjYsMi40LTksMCAgIGMtNy4yLTguNC0xNC40LTE2LjUtMjEuNi0yNC45Yy00LjUtNS4xLDEuMi0xMi4zLDYuMy0xMC44aDEzLjJjLTcuMi0yOC44LTI5LjEtNTIuNS01OC44LTU5LjFjLTUuNy0xLjItMTEuNC0xLjgtMTcuMS0xLjggICBjLTI4LjgsMC01NS41LDE1LjktNjkuNiw0MmMtMy45LDcuMi0xNC43LDAuOS0xMC44LTYuM2MxOS4yLTM0LjgsNTkuNC01NC45LDk4LjctNDYuMmMzNS40LDcuNSw2MywzNiw3MC4yLDcxLjQgICBDMTAzLjQtMzQ3LjYsMTE5LTM0Ny42LDExOS0zNDcuNnogTS04My4yLTMwOS4yYy01LjcsMC03LjgtNi42LTQuNS0xMC44YzcuMi04LjQsMTQuNC0xNi41LDIxLjYtMjQuOWMyLjEtMi40LDYuNi0yLjQsOSwwICAgYzcuMiw4LjQsMTQuNCwxNi41LDIxLjYsMjQuOWM0LjUsNS4xLTEuMiwxMi4zLTYuMywxMC44SC01NWM3LjIsMjguOCwyOS4xLDUyLjUsNTguOCw1OS4xYzUuNywxLjIsMTEuNCwxLjgsMTcuMSwxLjggICBjMjguOCwwLDU1LjUtMTUuOSw2OS42LTQyYzMuOS03LjIsMTQuNy0wLjksMTAuOCw2LjNjLTE5LjIsMzUuMS01OS40LDU1LjItOTguNyw0Ni41Yy0zNS40LTcuOC02My4zLTM2LjMtNzAuNS03MS43SC04My4yeiIvPgo8L2c+Cjwvc3ZnPgo=">
  7. </div>
  8. <div class="col-12 p-centered"></div>
  9. <div class="col-12 mt-3 p-centered" v-if="loading">
  10. <div class="col-3 col-sm-10 p-centered">
  11. <div class="loading loading-lg mt-3"></div>
  12. </div>
  13. </div>
  14. <transition name="fade" mode="out-in">
  15. <div class="col-12 mt-3 pt-2 p-centered" v-if="!loading && !uploading && OTAError !== null" key="error">
  16. <div class="col-3 col-sm-9 col-md-6 p-centered text-center">
  17. <svg width="32px" height="32px" style="vertical-align: middle;" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  18. <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
  19. <rect id="bound" x="0" y="0" width="24" height="24"></rect>
  20. <circle id="Oval-5" fill="#DF4759" opacity="0.3" cx="12" cy="12" r="10"></circle>
  21. <rect id="Rectangle-9" fill="#DF4759" x="11" y="7" width="2" height="8" rx="1"></rect>
  22. <rect id="Rectangle-9-Copy" fill="#DF4759" x="11" y="16" width="2" height="2" rx="1"></rect>
  23. </g>
  24. </svg>
  25. <span style="vertical-align: middle;" class="ml-2"> {{ OTAError }} </span>
  26. <br>
  27. <br>
  28. <div class="mt-3">
  29. <button class="btn btn-light mr-2" @click="clear">
  30. <svg xmlns="http://www.w3.org/2000/svg" class="pt-1" width="16px" height="16px" viewBox="0 0 24 24">
  31. <g data-name="Layer 2">
  32. <g data-name="arrow-back">
  33. <rect width="24" height="24" transform="rotate(90 12 12)" opacity="0" />
  34. <path
  35. fill="currentColor"
  36. d="M19 11H7.14l3.63-4.36a1 1 0 1 0-1.54-1.28l-5 6a1.19 1.19 0 0 0-.09.15c0 .05 0 .08-.07.13A1 1 0 0 0 4 12a1 1 0 0 0 .07.36c0 .05 0 .08.07.13a1.19 1.19 0 0 0 .09.15l5 6A1 1 0 0 0 10 19a1 1 0 0 0 .64-.23 1 1 0 0 0 .13-1.41L7.14 13H19a1 1 0 0 0 0-2z" />
  37. </g>
  38. </g>
  39. </svg>
  40. Back
  41. </button>
  42. <button class="btn btn-primary ml-2" @click="retryOTA">
  43. <svg xmlns="http://www.w3.org/2000/svg" class="pt-1" width="16px" height="16px" viewBox="0 0 24 24">
  44. <g data-name="Layer 2">
  45. <g data-name="refresh">
  46. <rect width="24" height="24" opacity="0" />
  47. <path
  48. fill="currentColor"
  49. d="M20.3 13.43a1 1 0 0 0-1.25.65A7.14 7.14 0 0 1 12.18 19 7.1 7.1 0 0 1 5 12a7.1 7.1 0 0 1 7.18-7 7.26 7.26 0 0 1 4.65 1.67l-2.17-.36a1 1 0 0 0-1.15.83 1 1 0 0 0 .83 1.15l4.24.7h.17a1 1 0 0 0 .34-.06.33.33 0 0 0 .1-.06.78.78 0 0 0 .2-.11l.09-.11c0-.05.09-.09.13-.15s0-.1.05-.14a1.34 1.34 0 0 0 .07-.18l.75-4a1 1 0 0 0-2-.38l-.27 1.45A9.21 9.21 0 0 0 12.18 3 9.1 9.1 0 0 0 3 12a9.1 9.1 0 0 0 9.18 9A9.12 9.12 0 0 0 21 14.68a1 1 0 0 0-.7-1.25z" />
  50. </g>
  51. </g>
  52. </svg>
  53. Retry
  54. </button>
  55. </div>
  56. </div>
  57. </div>
  58. <div class="col-12 mt-3 pt-2 p-centered" v-else-if="!loading && !uploading && OTASuccess" key="success">
  59. <div class="col-3 col-sm-9 col-md-6 p-centered text-center">
  60. <svg width="32px" height="32px" style="vertical-align: middle;" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  61. <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
  62. <rect id="bound" x="0" y="0" width="24" height="24"></rect>
  63. <circle id="Oval-5" fill="#42BA96" opacity="0.3" cx="12" cy="12" r="10"></circle>
  64. <path d="M16.7689447,7.81768175 C17.1457787,7.41393107 17.7785676,7.39211077 18.1823183,7.76894473 C18.5860689,8.1457787 18.6078892,8.77856757 18.2310553,9.18231825 L11.2310553,16.6823183 C10.8654446,17.0740439 10.2560456,17.107974 9.84920863,16.7592566 L6.34920863,13.7592566 C5.92988278,13.3998345 5.88132125,12.7685345 6.2407434,12.3492086 C6.60016555,11.9298828 7.23146553,11.8813212 7.65079137,12.2407434 L10.4229928,14.616916 L16.7689447,7.81768175 Z" id="Path-92" fill="#42BA96"></path>
  65. </g>
  66. </svg>
  67. <span style="vertical-align: middle;" class="ml-2 mb-2"> OTA Success </span>
  68. <br>
  69. <br>
  70. <button class="btn btn-primary mt-3" @click="clear">
  71. <svg xmlns="http://www.w3.org/2000/svg" class="pt-1" width="16px" height="16px" viewBox="0 0 24 24">
  72. <g data-name="Layer 2">
  73. <g data-name="arrow-back">
  74. <rect width="24" height="24" transform="rotate(90 12 12)" opacity="0" />
  75. <path
  76. fill="currentColor"
  77. d="M19 11H7.14l3.63-4.36a1 1 0 1 0-1.54-1.28l-5 6a1.19 1.19 0 0 0-.09.15c0 .05 0 .08-.07.13A1 1 0 0 0 4 12a1 1 0 0 0 .07.36c0 .05 0 .08.07.13a1.19 1.19 0 0 0 .09.15l5 6A1 1 0 0 0 10 19a1 1 0 0 0 .64-.23 1 1 0 0 0 .13-1.41L7.14 13H19a1 1 0 0 0 0-2z" />
  78. </g>
  79. </g>
  80. </svg>
  81. Back
  82. </button>
  83. </div>
  84. </div>
  85. <div class="col-12 mt-3 p-centered" v-else-if="!loading && !uploading" key="otainput">
  86. <div class="col-3 col-sm-9 col-md-6 p-centered">
  87. <div class="form-group pt-2 mt-2">
  88. <label class="form-radio form-inline mr-2">
  89. <input type="radio" name="firmwaretype" for="firmwaretype" value="firmware" v-model="type"><i class="form-icon"></i> Firmware
  90. </label>
  91. <label class="form-radio form-inline ml-2">
  92. <input type="radio" name="firmwaretype" for="firmwaretype" value="filesystem" v-model="type"><i class="form-icon"></i> Filesystem
  93. </label>
  94. </div>
  95. <div class="form-group pt-2 mt-3">
  96. <input class="form-input file-input" type="file" ref="file" accept=".bin,.bin.gz" @change="uploadOTA">
  97. </div>
  98. </div>
  99. </div>
  100. </transition>
  101. <transition name="fade" mode="out-in">
  102. <div class="col-12 mt-3 mb-2 pt-2 p-centered" v-if="!loading && uploading">
  103. <div class="col-2 mt-3 mb-2 col-sm-7 col-md-4 text-right p-centered">
  104. <div class="bar mt-3 bar-sm">
  105. <div class="bar-item tooltip" :data-tooltip="progress+'%'" :style="{ width: progress+'%' }"></div>
  106. </div>
  107. <div class="pt-2">{{progress}}%</div>
  108. </div>
  109. </div>
  110. </transition>
  111. <div class="col-12 mt-3 p-centered"></div>
  112. </div>
  113. <transition name="fade" mode="out-in">
  114. <div class="columns mt-3" v-if="!loading">
  115. <div class="col-12 text-center">
  116. <span class="label label-rounded mr-2">{{ deviceData.id }}</span> - <span class="label label-rounded label-primary ml-2">{{ deviceData.hardware }}</span>
  117. </div>
  118. </div>
  119. </transition>
  120. </main>
  121. </template>
  122. <script>
  123. export default {
  124. name: 'App',
  125. data() {
  126. return {
  127. loading: true,
  128. uploading: false,
  129. progress: 0,
  130. OTAError: null,
  131. OTASuccess: false,
  132. type: 'firmware',
  133. file: null,
  134. deviceData: {
  135. id: null,
  136. hardware: null,
  137. },
  138. };
  139. },
  140. methods: {
  141. fileMD5(file) {
  142. return new Promise((resolve, reject) => {
  143. const blobSlice = File.prototype.slice
  144. || File.prototype.mozSlice || File.prototype.webkitSlice;
  145. const chunkSize = 2097152; // Read in chunks of 2MB
  146. const chunks = Math.ceil(file.size / chunkSize);
  147. const spark = new this.SparkMD5.ArrayBuffer();
  148. const fileReader = new FileReader();
  149. let currentChunk = 0;
  150. let loadNext;
  151. fileReader.onload = (e) => {
  152. spark.append(e.target.result); // Append array buffer
  153. currentChunk += 1;
  154. if (currentChunk < chunks) {
  155. loadNext();
  156. } else {
  157. const md5 = spark.end();
  158. resolve(md5);
  159. }
  160. };
  161. fileReader.onerror = (e) => {
  162. reject(e);
  163. };
  164. loadNext = () => {
  165. const start = currentChunk * chunkSize;
  166. const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
  167. fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  168. };
  169. loadNext();
  170. });
  171. },
  172. uploadOTA(event) {
  173. this.uploading = true;
  174. const formData = new FormData();
  175. if (event !== null) {
  176. [this.file] = event.target.files;
  177. }
  178. const request = new XMLHttpRequest();
  179. request.addEventListener('load', () => {
  180. // request.response will hold the response from the server
  181. if (request.status === 200) {
  182. this.OTASuccess = true;
  183. } else if (request.status !== 500) {
  184. this.OTAError = `[HTTP ERROR] ${request.statusText}`;
  185. } else {
  186. this.OTAError = request.responseText;
  187. }
  188. this.uploading = false;
  189. this.progress = 0;
  190. });
  191. // Upload progress
  192. request.upload.addEventListener('progress', (e) => {
  193. this.progress = Math.trunc((e.loaded / e.total) * 100);
  194. });
  195. request.withCredentials = true;
  196. this.fileMD5(this.file)
  197. .then((md5) => {
  198. formData.append('MD5', md5);
  199. formData.append(this.type, this.file, this.type);
  200. request.open('post', '/update');
  201. request.send(formData);
  202. })
  203. .catch(() => {
  204. this.OTAError = 'Unknown error while upload, check the console for details.';
  205. this.uploading = false;
  206. this.progress = 0;
  207. });
  208. },
  209. retryOTA() {
  210. this.OTAError = null;
  211. this.OTASuccess = false;
  212. this.uploadOTA(null);
  213. },
  214. clear() {
  215. this.OTAError = null;
  216. this.OTASuccess = false;
  217. },
  218. },
  219. mounted() {
  220. this.deviceData = { id: '540985', hardware: 'ESP8266' };
  221. this.loading = false;
  222. },
  223. };
  224. </script>
  225. <style lang="scss">
  226. $primary-color: #488EFF;
  227. // Variables and mixins
  228. @import "~spectre.css/src/variables";
  229. @import "~spectre.css/src/mixins";
  230. /*! Spectre.css v#{$version} | MIT License | github.com/picturepan2/spectre */
  231. // Reset and dependencies
  232. @import "~spectre.css/src/normalize";
  233. @import "~spectre.css/src/base";
  234. // Elements
  235. @import "~spectre.css/src/typography";
  236. @import "~spectre.css/src/labels";
  237. @import "~spectre.css/src/buttons";
  238. @import "~spectre.css/src/tooltips";
  239. @import "~spectre.css/src/cards";
  240. @import "~spectre.css/src/bars";
  241. @import "~spectre.css/src/forms";
  242. @import "~spectre.css/src/layout";
  243. @import "~spectre.css/src/animations";
  244. @import "~spectre.css/src/utilities";
  245. .logo{
  246. width: 100%;
  247. max-width: 320px;
  248. }
  249. .card{
  250. border: 0;
  251. box-shadow: 0 0.25rem 1rem rgba(48,55,66,.1);
  252. border-radius: 0.275rem;
  253. }
  254. .label{
  255. font-size: 0.65rem !important;
  256. }
  257. .file-input{
  258. border-radius: 0.275rem;
  259. }
  260. .pt-3{
  261. padding-top: 32px;
  262. }
  263. .mt-3{
  264. margin-top: 24px;
  265. }
  266. .fade-enter-active, .fade-leave-active {
  267. transition: opacity .25s;
  268. }
  269. .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  270. opacity: 0;
  271. }
  272. </style>