Files
UpdaterWebLibrary/dist/index.cjs.js
2025-12-01 21:21:05 +00:00

269 lines
10 KiB
JavaScript

'use strict';
var parseXml = require('@rgrove/parse-xml');
/// <reference types="cordova-plugin-ble-central" />
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.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.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);
});
}
}
module.exports = Updater;