/// import { parseXml, XmlDocument, XmlElement, XmlText } from "@rgrove/parse-xml"; export type versionNotes = { title: string; date: Date | string; link: string; html: string; } export default class Updater { public archiveURL: string; public feedType: string; public bleObject: BLECentralPlugin.BLECentralPluginStatic; protected bleDeviceId: string; private readonly _updaterServiceUUID: string = "71a4438e-fd52-4b15-b3d2-ec0e3e561900"; private readonly _updaterVersionCharactersiticUUID: string = "71a4438e-fd52-4b15-b3d2-ec0e3e561910"; private readonly _updaterCommandCharacterisitcUUID: string = "71a4438e-fd52-4b15-b3d2-ec0e3e561920"; private readonly _updateFileCharacteristicUUID: string = "71a4438e-fd52-4b15-b3d2-ec0e3e561930"; public file: Int8Array; private _fileSize: number; private _fileProgress: number = 0; private _packetSize: number; constructor(archiveURL: string = "/", feedType: string = "atom", bleObject?: BLECentralPlugin.BLECentralPluginStatic, packetSize: number = 512) { this.archiveURL = archiveURL; this.feedType = feedType; if (bleObject) { this.bleObject = bleObject; } this._packetSize = packetSize; } /* FEEDS */ private async getRawArchive(): Promise { const res = await fetch(`http://cors.emaker.limited/?url=${this.archiveURL}.${this.feedType}`, { // "mode": "cors" }); const text = await res.text(); return text; } // atom feeds private atomGetVersionDetails(entry: XmlElement): versionNotes { let outEntry: versionNotes = { title: "", date: new Date, link: "", html: "" }; entry.children.forEach((elm) => { let element = elm as XmlElement; if (element.name == "title") { outEntry.title = (element.children[0] as XmlText).text; } else if (element.name == "updated") { outEntry.date = new Date((element.children[0] as XmlText).text); } else if (element.name == "link") { outEntry.link = element.attributes["href"]; } else if (element.name == "content") { outEntry.html = (element.children[0] as XmlText).text; } }) return outEntry; } private async atomGetArchive(): Promise { const rawArchive: string = await this.getRawArchive(); const releaseNotes: XmlDocument = parseXml(rawArchive); const output: versionNotes[] = []; // console.dir(releaseNotes) (releaseNotes.children[0] as XmlElement).children.forEach((elm) => { if (elm.type == "element") { const element = elm as XmlElement; if (element.name == "entry") { output.push(this.atomGetVersionDetails(element)) } } }); return output; } // rss feeds private rssGetVersionDetails(entry: XmlElement): versionNotes { let outEntry: versionNotes = { title: "", date: new Date, link: "", html: "" }; entry.children.forEach((elm) => { let element = elm as XmlElement; if (element.name == "title") { outEntry.title = (element.children[0] as XmlText).text; } else if (element.name == "pubDate") { outEntry.date = (element.children[0] as XmlText).text; } else if (element.name == "link") { outEntry.link = (element.children[0] as XmlText).text; } else if (element.name == "content") { outEntry.html = (element.children[0] as XmlText).text; } }) return outEntry; } private async rssGetArchive(): Promise { const rawArchive: string = await this.getRawArchive(); const releaseNotes: XmlDocument = parseXml(rawArchive); const output: versionNotes[] = []; ((releaseNotes.children[0] as XmlElement).children[1] as XmlElement).children.forEach((elm) => { if (elm.type == "element") { const element = elm as XmlElement; if (element.name == "item") { output.push(this.rssGetVersionDetails(element)) } } }) return output; } public async getArchive(): Promise { if (this.feedType == "atom") { return this.atomGetArchive() } else if (this.feedType == "rss") { return this.rssGetArchive() } } /* BLUETOOTH */ public setDeviceId(id: string): void { this.bleDeviceId = id; } private bytesToString(buffer: ArrayBuffer): string { return String.fromCharCode.apply(null, new Uint8Array(buffer)); } private async readVersionNumber(): Promise { return new Promise((resolve, reject) => { this.bleObject.read( this.bleDeviceId, this._updaterServiceUUID, this._updaterVersionCharactersiticUUID, (rawData: ArrayBuffer) => { resolve(this.bytesToString(rawData)); }, (error: string) => { reject(`Error: ${error}`); } ) }); } private async getLatestVersion(): Promise { let feed: versionNotes[] = await this.getArchive(); let newestDate: Date = feed[0].date as Date; let i: number = 0; feed.forEach((item: versionNotes, index: number) => { if (item.date > newestDate) { newestDate = item.date as Date; i = index; } }); return feed[i].title; } public async checkForUpdate(): Promise { // 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; } public async getBoardVersion(): Promise { return await this.readVersionNumber(); } /* FILE FLASHING */ public async getFirmware(version: versionNotes): Promise { 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; } } public getFileSize(): number { return this._fileSize; } private async sendNextPacket(): Promise { let packet = this.file.slice(this._fileProgress, this._fileProgress+this._packetSize); return new Promise((resolve, reject) => { this.bleObject.write(this.bleDeviceId, this._updaterServiceUUID, this._updateFileCharacteristicUUID, packet.buffer, () => { resolve(true); }, (error) => { reject(error); } ) }); } private async sendEndCmd(agree: boolean): Promise { 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 public async flashFirmware(progressCallback: (message: string) => void): Promise { // write filesize to board // await notify from cmd - ready // send a packet // check for error // write file length return new Promise(async (resolve, reject) => { const buffer = new ArrayBuffer(4) let view = new Int32Array(buffer); view[0] = this._fileSize; await this.bleObject.withPromises.write(this.bleDeviceId, this._updaterServiceUUID, this._updaterCommandCharacterisitcUUID, buffer); // start notify this.bleObject.startNotification(this.bleDeviceId, this._updaterServiceUUID, this._updaterCommandCharacterisitcUUID, async (rawData: ArrayBuffer): Promise => { let dataView = new Int8Array(rawData); if (dataView[0] == 1) { // send file await this.sendNextPacket(); progressCallback(`Sending (${Math.floor((this._fileProgress *100)/this._fileSize)})`); } else if (dataView[0] == 2) { // done logic if (this._fileProgress >= this._fileSize) { // send agree await 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 { // send disagree await this.sendEndCmd(false); progressCallback(`Error, starting over`); this._fileProgress = 0; } } else if (dataView[0] == 15) { // error cmd progressCallback(`Error on remote`); reject("Error on remote"); } else { // no command progressCallback(`Error on remote`); reject("Error: command does not exist"); } }, () => { reject("Error: Failed to start notify"); }); }); } }