import { parseXml } from '@rgrove/parse-xml'; /// class Updater { archiveURL; feedType; bleObject; bleDeviceId; _updaterServiceUUID = "71a4438e-fd52-4b15-b3d2-ec0e3e561900"; _updaterVersionCharactersiticUUID = "71a4438e-fd52-4b15-b3d2-ec0e3e561910"; _updaterCommandCharacterisitcUUID = "71a4438e-fd52-4b15-b3d2-ec0e3e561920"; _updateFileCharacteristicUUID = "71a4438e-fd52-4b15-b3d2-ec0e3e561930"; file; _fileSize; _fileProgress = 0; _packetSize; constructor(archiveURL = "/", feedType = "atom", bleObject, packetSize = 512) { this.archiveURL = archiveURL; this.feedType = feedType; if (bleObject) { this.bleObject = bleObject; } this._packetSize = packetSize; } /* FEEDS */ async getRawArchive() { const res = await fetch(`http://cors.emaker.limited/?url=${this.archiveURL}.${this.feedType}`, { // "mode": "cors" }); const text = await res.text(); return text; } // atom feeds atomGetVersionDetails(entry) { let outEntry = { title: "", date: new Date, link: "", html: "" }; entry.children.forEach((elm) => { let element = elm; if (element.name == "title") { outEntry.title = element.children[0].text; } else if (element.name == "updated") { outEntry.date = new Date(element.children[0].text); } else if (element.name == "link") { outEntry.link = element.attributes["href"]; } else if (element.name == "content") { outEntry.html = element.children[0].text; } }); return outEntry; } async atomGetArchive() { const rawArchive = await this.getRawArchive(); const releaseNotes = parseXml(rawArchive); const output = []; // console.dir(releaseNotes) releaseNotes.children[0].children.forEach((elm) => { if (elm.type == "element") { const element = elm; if (element.name == "entry") { output.push(this.atomGetVersionDetails(element)); } } }); return output; } // rss feeds rssGetVersionDetails(entry) { let outEntry = { title: "", date: new Date, link: "", html: "" }; entry.children.forEach((elm) => { let element = elm; if (element.name == "title") { outEntry.title = element.children[0].text; } else if (element.name == "pubDate") { outEntry.date = element.children[0].text; } else if (element.name == "link") { outEntry.link = element.children[0].text; } else if (element.name == "content") { outEntry.html = element.children[0].text; } }); return outEntry; } async rssGetArchive() { const rawArchive = await this.getRawArchive(); const releaseNotes = parseXml(rawArchive); const output = []; releaseNotes.children[0].children[1].children.forEach((elm) => { if (elm.type == "element") { const element = elm; if (element.name == "item") { output.push(this.rssGetVersionDetails(element)); } } }); return output; } async getArchive() { if (this.feedType == "atom") { return this.atomGetArchive(); } else if (this.feedType == "rss") { return this.rssGetArchive(); } } /* BLUETOOTH */ setDeviceId(id) { this.bleDeviceId = id; } bytesToString(buffer) { return String.fromCharCode.apply(null, new Uint8Array(buffer)); } async readVersionNumber() { return new Promise((resolve, reject) => { this.bleObject.read(this.bleDeviceId, this._updaterServiceUUID, this._updaterVersionCharactersiticUUID, (rawData) => { resolve(this.bytesToString(rawData)); }, (error) => { reject(`Error: ${error}`); }); }); } async getLatestVersion() { let feed = await this.getArchive(); let newestDate = feed[0].date; let i = 0; feed.forEach((item, index) => { if (item.date > newestDate) { newestDate = item.date; i = index; } }); return feed[i].title; } async checkForUpdate() { // read device value const deviceVersion = await this.readVersionNumber(); // compare with latest version const latestVersion = await this.getLatestVersion(); if (deviceVersion != latestVersion) { return true; } // update return false; } async getBoardVersion() { return await this.readVersionNumber(); } /* FILE FLASHING */ async getFirmware(version) { try { const res = await fetch(`http://cors.emaker.limited/?url=${this.archiveURL}/download/${version.title}/firmware.bin`); let buf = await res.arrayBuffer(); this.file = new Int8Array(buf); this._fileSize = this.file.byteLength; return true; } catch { return false; } } getFileSize() { return this._fileSize; } async sendNextPacket() { let packet = this.file.slice(this._fileProgress, this._fileProgress + this._packetSize); // this._fileProgress += this._packetSize; return new Promise((resolve, reject) => { this.bleObject.writeWithoutResponse(this.bleDeviceId, this._updaterServiceUUID, this._updateFileCharacteristicUUID, packet.buffer, () => { console.log(`Sent: ${packet}, progress: ${this._fileProgress}`); resolve(true); }, (error) => { this._fileProgress -= this._packetSize; reject(error); }); }); } async sendEndCmd(agree) { const buffer = new ArrayBuffer(1); let view = new Int8Array(buffer); view[0] = agree ? 3 : 4; await this.bleObject.withPromises.write(this.bleDeviceId, this._updaterServiceUUID, this._updaterCommandCharacterisitcUUID, buffer); return; } // start and autoconnect before you run this function, so that you can have an "uninterrupted" connection when the board reboots async flashFirmware(progressCallback) { // write filesize to board // await notify from cmd - ready // send a packet // check for error // write file length return new Promise(async (resolve, reject) => { // set mtu this._packetSize = await this.bleObject.withPromises.requestMtu(this.bleDeviceId, this._packetSize); // start notify this.bleObject.startNotification(this.bleDeviceId, this._updaterServiceUUID, this._updaterCommandCharacterisitcUUID, (rawData) => { let dataView = new Uint8Array(rawData); console.log(dataView); if (dataView[0] == 1 && dataView.length == 1) { // send file this.sendNextPacket(); progressCallback(`MTU: ${this._packetSize}; Sending (${Math.floor((this._fileProgress * 100) / this._fileSize)}%), ${this._fileProgress} / ${this._fileSize}`); } else if (dataView[0] == 2 && dataView.length == 1) { // done logic console.log(`progress >= filesize: ${this._fileProgress} vs ${this._fileSize}`); if (this._fileProgress >= (this._fileSize - this._packetSize)) { console.log("true"); // send agree this.sendEndCmd(true); progressCallback(`Complete!`); this.bleObject.stopNotification(this.bleDeviceId, this._updaterServiceUUID, this._updaterCommandCharacterisitcUUID, () => { // success resolve(true); }, (error) => { reject("Error: Failed to stop notify"); }); } else { console.log("False"); // send disagree this.sendEndCmd(false); progressCallback(`Error, starting over: ${this._fileProgress} / ${this._fileSize}`); this._fileProgress = 0; } } else if (dataView[0] == 15 && dataView.length == 1) { // error cmd progressCallback(`Error on remote: ${this._fileProgress} / ${this._fileSize}`); reject("Error on remote"); } else if (dataView[0] == 0 && dataView.length == 1) { // ignore no command progressCallback(`Board is on`); } else { // should be the file progress let fileProgressView = new Uint32Array(rawData); this._fileProgress = (dataView[3] << 24) | (dataView[2] << 16) | (dataView[1] << 8) | dataView[0]; this.sendNextPacket(); progressCallback(`Recieved progress ${fileProgressView[0]}; Sending (${Math.floor((this._fileProgress * 100) / this._fileSize)}%), ${this._fileProgress} / ${this._fileSize}`); } }, (error) => { reject("Error: Failed to start notify"); console.error(error); }); const buffer = new ArrayBuffer(4); let view = new Int32Array(buffer); view[0] = this._fileSize; this.bleObject.withPromises.write(this.bleDeviceId, this._updaterServiceUUID, this._updaterCommandCharacterisitcUUID, buffer); }); } } export { Updater as default };