app.js 8.8 KB

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