app.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. "use strict";
  2. /**
  3. * Constants for application
  4. *
  5. * manifestList is the url that contains all possible manifests
  6. * pollTime is used to check for changes in the serial ports
  7. *
  8. * @type {{manifestList: string, pollTime: number}}
  9. */
  10. const CONSTANTS = {
  11. manifestList: "http://flasher.thingssdk.com/v1/manifest-list.json",
  12. pollTime: 1000
  13. };
  14. var isFlashing = false;
  15. var last_notification = "";
  16. /************************
  17. * Backend dependencies.
  18. * Note: Paths are relative to index.html not app.js
  19. ************************/
  20. const SerialScanner = require("../back-end/serial_scanner");
  21. const PortSelect = require("./js/port_select");
  22. const prepareBinaries = require("../back-end/prepare_binaries");
  23. const log = require("../back-end/logger");
  24. const RomComm = require("../back-end/rom_comm");
  25. const serialScanner = new SerialScanner();
  26. /************************
  27. * UI Elements
  28. ************************/
  29. const flashButton = $("flash-button");
  30. const appStatus = $("status");
  31. const portsSelect = new PortSelect($("ports"));
  32. const manifestsSelect = $("manifests");
  33. const svg = $("Layer_1");
  34. const appWrapper = $("app");
  35. const logoWrapper = $("logo");
  36. const form = $("form");
  37. /************************
  38. * Utility Functions
  39. ************************/
  40. /**
  41. * Simple helper returns HTML element from an element's id
  42. *
  43. * @param id a string of the id of an HTML element
  44. * @returns {Element}
  45. */
  46. function $(id) { return document.getElementById(id); }
  47. /**
  48. * Processes the response from an HTTP fetch and returns the JSON promise.
  49. *
  50. * @param response from a fetch call
  51. * @returns {Promise}
  52. */
  53. function processJSON(response) {
  54. return response.json();
  55. }
  56. /************************
  57. * Handle UI
  58. ************************/
  59. flashButton.addEventListener("click", event => {
  60. isFlashing = true;
  61. disableInputs();
  62. prepareUIForFlashing(() => {
  63. fetch(manifestsSelect.value)
  64. .then(processJSON)
  65. .then(flashWithManifest);
  66. });
  67. });
  68. /************************
  69. * Manage serial port events
  70. ************************/
  71. serialScanner.on("ports", ports => {
  72. portsSelect.addAll(ports);
  73. });
  74. serialScanner.on("deviceAdded", port => {
  75. portsSelect.add(port);
  76. new Notification(`Added: ${port}!`);
  77. });
  78. serialScanner.on("deviceRemoved", port => {
  79. portsSelect.remove(port);
  80. new Notification(`Removed: ${port}!`);
  81. });
  82. serialScanner.on("error", err => {
  83. if(err.message === "No serial ports detected.") {
  84. if(portsSelect.children[0]) {
  85. portsSelect.remove(portsSelect.children[0].textContent);
  86. }
  87. }
  88. onError(err);
  89. });
  90. /**
  91. * Updates UI to say it's ready
  92. */
  93. function readyToFlash() {
  94. appStatus.textContent = "Ready";
  95. enableInputs();
  96. }
  97. /**
  98. * Enabled the serial port SELECT and flash BUTTON elements.
  99. */
  100. function enableInputs() {
  101. if(flashButton.disabled) {
  102. portsSelect.disabled = false;
  103. manifestsSelect.disabled = false;
  104. flashButton.disabled = false;
  105. }
  106. }
  107. function disableInputs() {
  108. if(!flashButton.disabled) {
  109. portsSelect.disabled = true;
  110. manifestsSelect.disabled = true;
  111. flashButton.disabled = true;
  112. }
  113. }
  114. /**
  115. * Generic catch all error. Shows notification at the moment.
  116. * @param error
  117. */
  118. function onError(error){
  119. if(last_notification !== error.message) {
  120. last_notification = error.message;
  121. new Notification(last_notification);
  122. }
  123. appStatus.textContent = error.message;
  124. }
  125. function generateManifestList(manifestsJSON) {
  126. manifestsJSON.options.forEach(option => {
  127. option.versions.forEach(version => {
  128. const optionElement = document.createElement("option");
  129. optionElement.textContent = `${option.name} - ${version.version}`;
  130. optionElement.value = version.manifest;
  131. manifestsSelect.appendChild(optionElement);
  132. manifestsSelect.disabled = false;
  133. });
  134. });
  135. }
  136. function getManifests() {
  137. appStatus.textContent = "Getting latest manifests.";
  138. // Break the cache to get the latest
  139. fetch(CONSTANTS.manifestList)
  140. .then(processJSON)
  141. .then(generateManifestList).catch(error => {
  142. setTimeout(getManifests, CONSTANTS.pollTime);
  143. });
  144. }
  145. function updateProgressUI(percent, display) {
  146. percent = Math.round(percent * 100);
  147. updateProgressBar(percent, svg);
  148. appStatus.textContent = `${display} - ${percent}%`;
  149. }
  150. function flashWithManifest(manifest) {
  151. appStatus.textContent = `Flashing ${portsSelect.value}`;
  152. prepareBinaries(manifest)
  153. .on("error", err => {
  154. isFlashing = false;
  155. onError(err);
  156. restoreUI();
  157. })
  158. .on("progress", progress => {
  159. //For the download/extract progress.
  160. updateProgressUI(progress.details.downloadedBytes / progress.details.downloadSize, progress.display);
  161. })
  162. .on("complete", flashSpec => {
  163. const esp = new RomComm({
  164. portName: portsSelect.value,
  165. baudRate: 115200
  166. });
  167. esp.on('progress', progress => {
  168. updateProgressUI(progress.details.flashedBytes/progress.details.totalBytes, progress.display);
  169. });
  170. esp.open().then(result => {
  171. appStatus.textContent = `Flashing device connected to ${portsSelect.value}`;
  172. let promise = Promise.resolve();
  173. return esp.flashSpecifications(flashSpec)
  174. .then(() => esp.close())
  175. .then((result) => {
  176. new Notification("Flash Finished!");
  177. isFlashing = false;
  178. restoreUI();
  179. log.info("Flashed to latest Espruino build!", result);
  180. });
  181. }).catch(err => {
  182. isFlashing = false;
  183. esp.close();
  184. log.error("Oh noes!", err);
  185. onError(err);
  186. restoreUI();
  187. });
  188. });
  189. }
  190. function cloneSVGNode(node) {
  191. return node.cloneNode(true);
  192. }
  193. function updateClass(node) {
  194. node.setAttribute("class", "bg");
  195. return node;
  196. }
  197. function updateProgressBar(percent, svg){
  198. const line = svg.getElementsByClassName("st0")[0];
  199. const startDot = svg.getElementsByClassName("st1")[0];
  200. const finishDot = svg.getElementsByClassName("st2")[0];
  201. let backgroundElements = svg.getElementsByClassName("bg");
  202. if(backgroundElements.length === 0) {
  203. const g = svg.getElementsByTagName("g")[0];
  204. backgroundElements = [line, startDot, finishDot]
  205. .map(cloneSVGNode)
  206. .map(updateClass);
  207. backgroundElements.forEach(element => g.insertBefore(element, line));
  208. }
  209. const bgLine = backgroundElements[0];
  210. line.points.clear();
  211. if( percent < 1 ) {
  212. startDot.style.opacity = 0;
  213. } else {
  214. startDot.style.opacity = 1;
  215. }
  216. if( percent > 99 ) {
  217. finishDot.style.opacity = 1;
  218. } else {
  219. finishDot.style.opacity = 0;
  220. }
  221. for(var i = 0; i < percent * (bgLine.points.numberOfItems / 100); i ++) {
  222. if(i < bgLine.points.numberOfItems) {
  223. const point = bgLine.points.getItem(i);
  224. const newPoint = svg.createSVGPoint();
  225. newPoint.x = point.x;
  226. newPoint.y = point.y;
  227. line.points.appendItem(newPoint);
  228. }
  229. }
  230. }
  231. function prepareUIForFlashing(callback) {
  232. let percent = 100;
  233. appWrapper.classList.remove("finished");
  234. appWrapper.classList.add("flashing");
  235. let percentInterval = setInterval(() => {
  236. percent -= 1;
  237. updateProgressBar(percent, svg);
  238. if(percent === 0) {
  239. clearInterval(percentInterval);
  240. if(callback) callback();
  241. }
  242. }, 1);
  243. }
  244. function restoreUI(callback) {
  245. appWrapper.classList.remove("flashing");
  246. appWrapper.classList.add("finished");
  247. updateProgressBar(100, svg);
  248. if(callback) callback();
  249. }
  250. function inputStateManager() {
  251. if(!isFlashing) {
  252. if( manifestsSelect.children.length > 0 && portsSelect.children.length > 0 ) {
  253. readyToFlash();
  254. } else {
  255. disableInputs();
  256. }
  257. } else {
  258. disableInputs();
  259. }
  260. }
  261. /**
  262. * Get's manifest list for possibilities for flashing,
  263. * scans serial ports and sets up timer for checking for changes.
  264. */
  265. function start() {
  266. getManifests();
  267. serialScanner.scan();
  268. setInterval(() => {
  269. serialScanner.checkForChanges();
  270. inputStateManager();
  271. }, CONSTANTS.pollTime);
  272. }
  273. /**
  274. * Start Application
  275. */
  276. start();