1242 lines
31 KiB
JavaScript
1242 lines
31 KiB
JavaScript
class Utils {
|
||
|
||
/**
|
||
* Waits for the provided milliseconds, and then resolves.
|
||
* @param millis
|
||
* @returns {Promise<void>}
|
||
*/
|
||
static async sleepMillis(millis) {
|
||
await new Promise((resolve) => {
|
||
setTimeout(resolve, millis);
|
||
});
|
||
}
|
||
|
||
static bytesToHex(bytes) {
|
||
for(var hex = [], i = 0; i < bytes.length; i++){
|
||
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
|
||
hex.push((current >>> 4).toString(16));
|
||
hex.push((current & 0xF).toString(16));
|
||
}
|
||
return hex.join("");
|
||
}
|
||
|
||
static md5(data) {
|
||
var bytes = [];
|
||
const hash = CryptoJS.MD5(CryptoJS.enc.Hex.parse(this.bytesToHex(data)));
|
||
for(var i = 0; i < hash.sigBytes; i++){
|
||
bytes.push((hash.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
|
||
}
|
||
return bytes;
|
||
}
|
||
|
||
static packUInt32BE(value) {
|
||
const buffer = new ArrayBuffer(4);
|
||
const view = new DataView(buffer);
|
||
view.setUint32(0, value, false);
|
||
return new Uint8Array(buffer);
|
||
}
|
||
|
||
static unpackUInt32BE(byteArray) {
|
||
const buffer = new Uint8Array(byteArray).buffer;
|
||
const view = new DataView(buffer);
|
||
return view.getUint32(0, false);
|
||
}
|
||
|
||
}
|
||
|
||
class RNode {
|
||
|
||
KISS_FEND = 0xC0;
|
||
KISS_FESC = 0xDB;
|
||
KISS_TFEND = 0xDC;
|
||
KISS_TFESC = 0xDD;
|
||
|
||
CMD_FREQUENCY = 0x01;
|
||
CMD_BANDWIDTH = 0x02;
|
||
CMD_TXPOWER = 0x03;
|
||
CMD_SF = 0x04;
|
||
CMD_CR = 0x05;
|
||
CMD_RADIO_STATE = 0x06;
|
||
|
||
CMD_STAT_RX = 0x21;
|
||
CMD_STAT_TX = 0x22
|
||
CMD_STAT_RSSI = 0x23;
|
||
CMD_STAT_SNR = 0x24;
|
||
|
||
CMD_BOARD = 0x47;
|
||
CMD_PLATFORM = 0x48;
|
||
CMD_MCU = 0x49;
|
||
CMD_RESET = 0x55;
|
||
CMD_RESET_BYTE = 0xF8;
|
||
CMD_DEV_HASH = 0x56;
|
||
CMD_FW_VERSION = 0x50;
|
||
CMD_ROM_READ = 0x51;
|
||
CMD_ROM_WRITE = 0x52;
|
||
CMD_CONF_SAVE = 0x53;
|
||
CMD_CONF_DELETE = 0x54;
|
||
CMD_FW_HASH = 0x58;
|
||
CMD_UNLOCK_ROM = 0x59;
|
||
ROM_UNLOCK_BYTE = 0xF8;
|
||
CMD_HASHES = 0x60;
|
||
CMD_FW_UPD = 0x61;
|
||
CMD_DISP_ROT = 0x67;
|
||
CMD_DISP_INT = 0x45;
|
||
CMD_DISP_BLNK = 0x64;
|
||
CMD_DISP_RCND = 0x68;
|
||
CMD_WIFI_MODE = 0x6A;
|
||
CMD_WIFI_SSID = 0x6B;
|
||
CMD_WIFI_PSK = 0x6C;
|
||
CMD_WIFI_CHN = 0x6E;
|
||
CMD_WIFI_IP = 0x84;
|
||
CMD_WIFI_NM = 0x85;
|
||
|
||
CMD_BT_CTRL = 0x46;
|
||
CMD_BT_PIN = 0x62;
|
||
|
||
CMD_DISP_READ = 0x66;
|
||
|
||
CMD_DETECT = 0x08;
|
||
DETECT_REQ = 0x73;
|
||
DETECT_RESP = 0x46;
|
||
|
||
RADIO_STATE_OFF = 0x00;
|
||
RADIO_STATE_ON = 0x01;
|
||
RADIO_STATE_ASK = 0xFF;
|
||
|
||
CMD_ERROR = 0x90
|
||
ERROR_INITRADIO = 0x01
|
||
ERROR_TXFAILED = 0x02
|
||
ERROR_EEPROM_LOCKED = 0x03
|
||
|
||
PLATFORM_AVR = 0x90;
|
||
PLATFORM_ESP32 = 0x80;
|
||
PLATFORM_NRF52 = 0x70;
|
||
|
||
MCU_1284P = 0x91;
|
||
MCU_2560 = 0x92;
|
||
MCU_ESP32 = 0x81;
|
||
MCU_NRF52 = 0x71;
|
||
|
||
BOARD_RNODE = 0x31;
|
||
BOARD_HMBRW = 0x32;
|
||
BOARD_TBEAM = 0x33;
|
||
BOARD_HUZZAH32 = 0x34;
|
||
BOARD_GENERIC_ESP32 = 0x35;
|
||
BOARD_LORA32_V2_0 = 0x36;
|
||
BOARD_LORA32_V2_1 = 0x37;
|
||
BOARD_RAK4631 = 0x51;
|
||
BOARD_XIAO_S3 = 0x3E;
|
||
|
||
HASH_TYPE_TARGET_FIRMWARE = 0x01;
|
||
HASH_TYPE_FIRMWARE = 0x02;
|
||
|
||
constructor(serialPort) {
|
||
this.serialPort = serialPort;
|
||
this.reader = serialPort.readable.getReader();
|
||
this.writable = serialPort.writable;
|
||
this.callbacks = {};
|
||
this.readLoop();
|
||
}
|
||
|
||
static async fromSerialPort(serialPort) {
|
||
|
||
// open port
|
||
await serialPort.open({
|
||
baudRate: 115200,
|
||
});
|
||
|
||
return new RNode(serialPort);
|
||
|
||
}
|
||
|
||
async close() {
|
||
|
||
// release reader lock
|
||
try {
|
||
this.reader.releaseLock();
|
||
} catch(e) {
|
||
//console.log("failed to release lock on serial port readable, ignoring...", e);
|
||
}
|
||
|
||
// close serial port
|
||
try {
|
||
await this.serialPort.close();
|
||
} catch(e) {
|
||
//console.log("failed to close serial port, ignoring...", e);
|
||
}
|
||
|
||
}
|
||
|
||
async write(bytes) {
|
||
const writer = this.writable.getWriter();
|
||
try {
|
||
await writer.write(new Uint8Array(bytes));
|
||
} finally {
|
||
writer.releaseLock();
|
||
}
|
||
}
|
||
|
||
async readLoop() {
|
||
try {
|
||
let buffer = [];
|
||
let inFrame = false;
|
||
while(true){
|
||
|
||
// read kiss frames until reader indicates it's done
|
||
const { value, done } = await this.reader.read();
|
||
if(done){
|
||
break;
|
||
}
|
||
|
||
// read kiss frames
|
||
for(const byte of value){
|
||
if(byte === this.KISS_FEND){
|
||
if(inFrame){
|
||
// End of frame
|
||
const decodedFrame = this.decodeKissFrame(buffer);
|
||
if(decodedFrame){
|
||
this.onCommandReceived(decodedFrame);
|
||
} else {
|
||
console.warn("Invalid frame ignored.");
|
||
}
|
||
buffer = [];
|
||
}
|
||
inFrame = !inFrame;
|
||
} else if(inFrame) {
|
||
buffer.push(byte);
|
||
}
|
||
}
|
||
|
||
}
|
||
} catch(error) {
|
||
|
||
// ignore error if reader was released
|
||
if(error instanceof TypeError){
|
||
return;
|
||
}
|
||
|
||
console.error('Error reading from serial port: ', error);
|
||
|
||
} finally {
|
||
this.reader.releaseLock();
|
||
}
|
||
}
|
||
|
||
onCommandReceived(data) {
|
||
try {
|
||
|
||
// get received command and bytes from data
|
||
const [ command, ...bytes ] = data;
|
||
console.log("onCommandReceived", "0x" + command.toString(16), bytes);
|
||
|
||
// find callback for received command
|
||
const callback = this.callbacks[command];
|
||
if(!callback){
|
||
return;
|
||
}
|
||
|
||
// fire callback
|
||
callback(bytes);
|
||
|
||
// forget callback
|
||
delete this.callbacks[command];
|
||
|
||
} catch(e) {
|
||
console.log("failed to handle received command", data, e);
|
||
}
|
||
}
|
||
|
||
decodeKissFrame(frame) {
|
||
|
||
const data = [];
|
||
let escaping = false;
|
||
|
||
for(const byte of frame){
|
||
if(escaping){
|
||
if(byte === this.KISS_TFEND){
|
||
data.push(this.KISS_FEND);
|
||
} else if(byte === this.KISS_TFESC) {
|
||
data.push(this.KISS_FESC);
|
||
} else {
|
||
return null; // Invalid escape sequence
|
||
}
|
||
escaping = false;
|
||
} else if(byte === this.KISS_FESC) {
|
||
escaping = true;
|
||
} else {
|
||
data.push(byte);
|
||
}
|
||
}
|
||
|
||
// return null if incomplete escape at end
|
||
return escaping ? null : data;
|
||
|
||
}
|
||
|
||
createKissFrame(data) {
|
||
let frame = [this.KISS_FEND];
|
||
for(let byte of data){
|
||
if(byte === this.KISS_FEND){
|
||
frame.push(this.KISS_FESC, this.KISS_TFEND);
|
||
} else if(byte === this.KISS_FESC){
|
||
frame.push(this.KISS_FESC, this.KISS_TFESC);
|
||
} else {
|
||
frame.push(byte);
|
||
}
|
||
}
|
||
frame.push(this.KISS_FEND);
|
||
return new Uint8Array(frame);
|
||
}
|
||
|
||
async sendKissCommand(data) {
|
||
await this.write(this.createKissFrame(data));
|
||
}
|
||
|
||
// sends a command to the rnode, and resolves the promise with the result
|
||
async sendCommand(command, data) {
|
||
return new Promise(async (resolve, reject) => {
|
||
try {
|
||
|
||
// listen for response
|
||
this.callbacks[command] = (response) => {
|
||
resolve(response);
|
||
};
|
||
|
||
// send command
|
||
await this.sendKissCommand([
|
||
command,
|
||
...data,
|
||
]);
|
||
|
||
} catch(e) {
|
||
reject(e);
|
||
}
|
||
});
|
||
}
|
||
|
||
async reset() {
|
||
await this.sendKissCommand([
|
||
this.CMD_RESET,
|
||
this.CMD_RESET_BYTE,
|
||
]);
|
||
}
|
||
|
||
async detect() {
|
||
return new Promise(async (resolve) => {
|
||
try {
|
||
|
||
// timeout after provided millis
|
||
const timeout = setTimeout(() => {
|
||
resolve(false);
|
||
}, 2000);
|
||
|
||
// detect rnode
|
||
const response = await this.sendCommand(this.CMD_DETECT, [
|
||
this.DETECT_REQ,
|
||
]);
|
||
|
||
// we no longer want to timeout
|
||
clearTimeout(timeout);
|
||
|
||
// device is an rnode if response is as expected
|
||
const [ responseByte ] = response;
|
||
const isRnode = responseByte === this.DETECT_RESP;
|
||
resolve(isRnode);
|
||
|
||
} catch(e) {
|
||
resolve(false);
|
||
}
|
||
});
|
||
|
||
}
|
||
|
||
async getFirmwareVersion() {
|
||
|
||
const response = await this.sendCommand(this.CMD_FW_VERSION, [
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
var [ majorVersion, minorVersion ] = response;
|
||
if(minorVersion.length === 1){
|
||
minorVersion = "0" + minorVersion;
|
||
}
|
||
|
||
// 1.23
|
||
return majorVersion + "." + minorVersion;
|
||
|
||
}
|
||
|
||
async getPlatform() {
|
||
|
||
const response = await this.sendCommand(this.CMD_PLATFORM, [
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ platformByte ] = response;
|
||
return platformByte;
|
||
|
||
}
|
||
|
||
async getMcu() {
|
||
|
||
const response = await this.sendCommand(this.CMD_MCU, [
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ mcuByte ] = response;
|
||
return mcuByte;
|
||
|
||
}
|
||
|
||
async getBoard() {
|
||
|
||
const response = await this.sendCommand(this.CMD_BOARD, [
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ boardByte ] = response;
|
||
return boardByte;
|
||
|
||
}
|
||
|
||
async getDeviceHash() {
|
||
|
||
const response = await this.sendCommand(this.CMD_DEV_HASH, [
|
||
0x01, // anything != 0x00
|
||
]);
|
||
|
||
// read response from device
|
||
const [ ...deviceHash ] = response;
|
||
return deviceHash;
|
||
|
||
}
|
||
|
||
async getTargetFirmwareHash() {
|
||
|
||
const response = await this.sendCommand(this.CMD_HASHES, [
|
||
this.HASH_TYPE_TARGET_FIRMWARE,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ hashType, ...targetFirmwareHash ] = response;
|
||
return targetFirmwareHash;
|
||
|
||
}
|
||
|
||
async getFirmwareHash() {
|
||
|
||
const response = await this.sendCommand(this.CMD_HASHES, [
|
||
this.HASH_TYPE_FIRMWARE,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ hashType, ...firmwareHash ] = response;
|
||
return firmwareHash;
|
||
|
||
}
|
||
|
||
async getRom() {
|
||
|
||
const response = await this.sendCommand(this.CMD_ROM_READ, [
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ ...eepromBytes ] = response;
|
||
return eepromBytes;
|
||
|
||
}
|
||
|
||
async getFrequency() {
|
||
|
||
const response = await this.sendCommand(this.CMD_FREQUENCY, [
|
||
// request frequency by sending zero as 4 bytes
|
||
0x00,
|
||
0x00,
|
||
0x00,
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ ...frequencyBytes ] = response;
|
||
|
||
// convert 4 bytes to 32bit integer representing frequency in hertz
|
||
const frequencyInHz = frequencyBytes[0] << 24 | frequencyBytes[1] << 16 | frequencyBytes[2] << 8 | frequencyBytes[3];
|
||
return frequencyInHz;
|
||
|
||
}
|
||
|
||
async getBandwidth() {
|
||
|
||
const response = await this.sendCommand(this.CMD_BANDWIDTH, [
|
||
// request bandwidth by sending zero as 4 bytes
|
||
0x00,
|
||
0x00,
|
||
0x00,
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ ...bandwidthBytes ] = response;
|
||
|
||
// convert 4 bytes to 32bit integer representing bandwidth in hertz
|
||
const bandwidthInHz = bandwidthBytes[0] << 24 | bandwidthBytes[1] << 16 | bandwidthBytes[2] << 8 | bandwidthBytes[3];
|
||
return bandwidthInHz;
|
||
|
||
}
|
||
|
||
async getTxPower() {
|
||
|
||
const response = await this.sendCommand(this.CMD_TXPOWER, [
|
||
0xFF, // request tx power
|
||
]);
|
||
|
||
// read response from device
|
||
const [ txPower ] = response;
|
||
return txPower;
|
||
|
||
}
|
||
|
||
async getSpreadingFactor() {
|
||
|
||
const response = await this.sendCommand(this.CMD_SF, [
|
||
0xFF, // request spreading factor
|
||
]);
|
||
|
||
// read response from device
|
||
const [ spreadingFactor ] = response;
|
||
return spreadingFactor;
|
||
|
||
}
|
||
|
||
async getCodingRate() {
|
||
|
||
const response = await this.sendCommand(this.CMD_CR, [
|
||
0xFF, // request coding rate
|
||
]);
|
||
|
||
// read response from device
|
||
const [ codingRate ] = response;
|
||
return codingRate;
|
||
|
||
}
|
||
|
||
async getRadioState() {
|
||
|
||
const response = await this.sendCommand(this.CMD_RADIO_STATE, [
|
||
0xFF, // request radio state
|
||
]);
|
||
|
||
// read response from device
|
||
const [ radioState ] = response;
|
||
return radioState;
|
||
|
||
}
|
||
|
||
async getRxStat() {
|
||
|
||
const response = await this.sendCommand(this.CMD_STAT_RX, [
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ ...statBytes ] = response;
|
||
|
||
// convert 4 bytes to 32bit integer
|
||
const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3];
|
||
return stat;
|
||
|
||
}
|
||
|
||
async getTxStat() {
|
||
|
||
const response = await this.sendCommand(this.CMD_STAT_TX, [
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ ...statBytes ] = response;
|
||
|
||
// convert 4 bytes to 32bit integer
|
||
const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3];
|
||
return stat;
|
||
|
||
}
|
||
|
||
async getRssiStat() {
|
||
|
||
const response = await this.sendCommand(this.CMD_STAT_RSSI, [
|
||
0x00,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ rssi ] = response;
|
||
return rssi;
|
||
|
||
}
|
||
|
||
async disableBluetooth() {
|
||
await this.sendKissCommand([
|
||
this.CMD_BT_CTRL,
|
||
0x00, // stop
|
||
]);
|
||
}
|
||
|
||
async enableBluetooth() {
|
||
await this.sendKissCommand([
|
||
this.CMD_BT_CTRL,
|
||
0x01, // start
|
||
]);
|
||
}
|
||
|
||
async startBluetoothPairing(pinCallback) {
|
||
|
||
// listen for bluetooth pin
|
||
// pin will be available once the user has initiated pairing from an Android device
|
||
this.callbacks[this.CMD_BT_PIN] = (response) => {
|
||
|
||
// read response from device
|
||
const [ ...pinBytes ] = response;
|
||
|
||
// convert 4 bytes to 32bit integer
|
||
const pin = pinBytes[0] << 24 | pinBytes[1] << 16 | pinBytes[2] << 8 | pinBytes[3];
|
||
|
||
// tell user what the bluetooth pin is
|
||
console.log("Bluetooth Pairing Pin: " + pin);
|
||
pinCallback(pin);
|
||
|
||
};
|
||
|
||
// enable pairing
|
||
await this.sendKissCommand([
|
||
this.CMD_BT_CTRL,
|
||
0x02, // enable pairing
|
||
]);
|
||
|
||
}
|
||
|
||
async enableWiFiMode(wifiMode) {
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_MODE,
|
||
wifiMode, // 0x00 = OFF, 0x01 = AP, 0x02 = STATION
|
||
]);
|
||
}
|
||
|
||
async disableWiFiMode() {
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_MODE,
|
||
0x00, // OFF
|
||
]);
|
||
}
|
||
|
||
async setWiFiChannel(wifiChannel) {
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_CHN,
|
||
wifiChannel, // 1-14
|
||
]);
|
||
}
|
||
|
||
async setWiFiSSID(wifiSSID) {
|
||
const encoder = new TextEncoder();
|
||
const ssidBytes = encoder.encode(wifiSSID);
|
||
|
||
// Add null terminator
|
||
const data = new Uint8Array(ssidBytes.length + 1);
|
||
data.set(ssidBytes);
|
||
data[data.length - 1] = 0x00;
|
||
|
||
// KISS escape
|
||
const escaped = [];
|
||
for (const byte of data) {
|
||
if (byte === this.FEND) {
|
||
escaped.push(this.FESC, this.TFEND);
|
||
} else if (byte === this.FESC) {
|
||
escaped.push(this.FESC, this.TFESC);
|
||
} else {
|
||
escaped.push(byte);
|
||
}
|
||
}
|
||
|
||
// Send command
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_SSID,
|
||
...escaped,
|
||
]);
|
||
}
|
||
|
||
async setWiFiPSK(wifiPSK) {
|
||
if (wifiPSK == null) {
|
||
// Clear PSK
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_PSK,
|
||
0x00
|
||
]);
|
||
return;
|
||
}
|
||
|
||
const encoder = new TextEncoder();
|
||
const pskBytes = encoder.encode(wifiPSK);
|
||
|
||
// Enforce firmware length rules (8–32 characters)
|
||
if (pskBytes.length < 8 || pskBytes.length > 32) {
|
||
throw new Error("Invalid PSK length (must be 8–32 bytes)");
|
||
}
|
||
|
||
// Add null terminator
|
||
const data = new Uint8Array(pskBytes.length + 1);
|
||
data.set(pskBytes);
|
||
data[data.length - 1] = 0x00;
|
||
|
||
// KISS escape
|
||
const escaped = [];
|
||
for (const byte of data) {
|
||
if (byte === this.FEND) {
|
||
escaped.push(this.FESC, this.TFEND);
|
||
} else if (byte === this.FESC) {
|
||
escaped.push(this.FESC, this.TFESC);
|
||
} else {
|
||
escaped.push(byte);
|
||
}
|
||
}
|
||
|
||
// Send command
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_PSK,
|
||
...escaped,
|
||
]);
|
||
}
|
||
|
||
async setWiFiIP(wifiIP) {
|
||
// If null → clear IP to 0.0.0.0
|
||
if (wifiIP == null) {
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_IP,
|
||
0x00, 0x00, 0x00, 0x00
|
||
]);
|
||
return;
|
||
}
|
||
|
||
// Ensure it's a string
|
||
if (typeof wifiIP !== "string") {
|
||
throw new TypeError("Invalid IP address (not a string)");
|
||
}
|
||
|
||
// Split into octets
|
||
const parts = wifiIP.trim().split(".");
|
||
if (parts.length !== 4) {
|
||
throw new Error("Invalid IP address format");
|
||
}
|
||
|
||
// Convert to byte array
|
||
const ipBytes = new Uint8Array(4);
|
||
|
||
for (let i = 0; i < 4; i++) {
|
||
const value = Number(parts[i]);
|
||
|
||
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
||
throw new Error(`Invalid IP octet: ${parts[i]}`);
|
||
}
|
||
|
||
ipBytes[i] = value;
|
||
}
|
||
|
||
// Optional debug check
|
||
console.log("Setting WiFi IP bytes:", [...ipBytes]);
|
||
|
||
// Send command (no manual escaping)
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_IP,
|
||
...ipBytes
|
||
]);
|
||
}
|
||
|
||
async setWiFiNM(wifiNM) {
|
||
if (wifiNM == null) {
|
||
// Clear netmask → 0.0.0.0
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_NM,
|
||
0x00, 0x00, 0x00, 0x00
|
||
]);
|
||
return;
|
||
}
|
||
|
||
if (typeof wifiNM !== "string") {
|
||
throw new TypeError("Invalid netmask (not a string)");
|
||
}
|
||
|
||
const octets = wifiNM.split(".");
|
||
if (octets.length !== 4) {
|
||
throw new Error("Invalid netmask length");
|
||
}
|
||
|
||
const nmBytes = new Uint8Array(4);
|
||
|
||
for (let i = 0; i < 4; i++) {
|
||
const octet = Number(octets[i]);
|
||
|
||
if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
|
||
throw new Error("Invalid netmask octet value");
|
||
}
|
||
|
||
nmBytes[i] = octet;
|
||
}
|
||
|
||
// Send using same KISS pipeline as PSK/IP
|
||
await this.sendKissCommand([
|
||
this.CMD_WIFI_NM,
|
||
...nmBytes
|
||
]);
|
||
}
|
||
|
||
async readDisplay() {
|
||
|
||
const response = await this.sendCommand(this.CMD_DISP_READ, [
|
||
0x01,
|
||
]);
|
||
|
||
// read response from device
|
||
const [ ...displayBuffer ] = response;
|
||
return displayBuffer;
|
||
|
||
}
|
||
|
||
async setFrequency(frequencyInHz) {
|
||
|
||
const c1 = frequencyInHz >> 24;
|
||
const c2 = frequencyInHz >> 16 & 0xFF;
|
||
const c3 = frequencyInHz >> 8 & 0xFF;
|
||
const c4 = frequencyInHz & 0xFF;
|
||
|
||
await this.sendKissCommand([
|
||
this.CMD_FREQUENCY,
|
||
c1,
|
||
c2,
|
||
c3,
|
||
c4,
|
||
]);
|
||
|
||
}
|
||
|
||
async setBandwidth(bandwidthInHz) {
|
||
|
||
const c1 = bandwidthInHz >> 24;
|
||
const c2 = bandwidthInHz >> 16 & 0xFF;
|
||
const c3 = bandwidthInHz >> 8 & 0xFF;
|
||
const c4 = bandwidthInHz & 0xFF;
|
||
|
||
await this.sendKissCommand([
|
||
this.CMD_BANDWIDTH,
|
||
c1,
|
||
c2,
|
||
c3,
|
||
c4,
|
||
]);
|
||
|
||
}
|
||
|
||
async setTxPower(db) {
|
||
await this.sendKissCommand([
|
||
this.CMD_TXPOWER,
|
||
db,
|
||
]);
|
||
}
|
||
|
||
async setSpreadingFactor(spreadingFactor) {
|
||
await this.sendKissCommand([
|
||
this.CMD_SF,
|
||
spreadingFactor,
|
||
]);
|
||
}
|
||
|
||
async setCodingRate(codingRate) {
|
||
await this.sendKissCommand([
|
||
this.CMD_CR,
|
||
codingRate,
|
||
]);
|
||
}
|
||
|
||
async setRadioStateOn() {
|
||
await this.sendKissCommand([
|
||
this.CMD_RADIO_STATE,
|
||
this.RADIO_STATE_ON,
|
||
]);
|
||
}
|
||
|
||
async setRadioStateOff() {
|
||
await this.sendKissCommand([
|
||
this.CMD_RADIO_STATE,
|
||
this.RADIO_STATE_OFF,
|
||
]);
|
||
}
|
||
|
||
// setTNCMode
|
||
async saveConfig() {
|
||
await this.sendKissCommand([
|
||
this.CMD_CONF_SAVE,
|
||
0x00,
|
||
]);
|
||
}
|
||
|
||
// setNormalMode
|
||
async deleteConfig() {
|
||
await this.sendKissCommand([
|
||
this.CMD_CONF_DELETE,
|
||
0x00,
|
||
]);
|
||
}
|
||
|
||
async indicateFirmwareUpdate() {
|
||
await this.sendKissCommand([
|
||
this.CMD_FW_UPD,
|
||
0x01,
|
||
]);
|
||
}
|
||
|
||
async setFirmwareHash(hash) {
|
||
await this.sendKissCommand([
|
||
this.CMD_FW_HASH,
|
||
...hash,
|
||
]);
|
||
}
|
||
|
||
async writeRom(address, value) {
|
||
|
||
// write to rom
|
||
await this.sendKissCommand([
|
||
this.CMD_ROM_WRITE,
|
||
address,
|
||
value,
|
||
]);
|
||
|
||
// wait a bit to allow device to write to rom
|
||
await Utils.sleepMillis(85);
|
||
|
||
}
|
||
|
||
async wipeRom() {
|
||
|
||
await this.sendKissCommand([
|
||
this.CMD_UNLOCK_ROM,
|
||
this.ROM_UNLOCK_BYTE,
|
||
]);
|
||
|
||
// wiping can take up to 30 seconds
|
||
await Utils.sleepMillis(30000);
|
||
|
||
}
|
||
|
||
async getRomAsObject() {
|
||
const rom = await this.getRom();
|
||
return new ROM(rom);
|
||
}
|
||
|
||
async setDisplayRotation(rotation) {
|
||
await this.sendKissCommand([
|
||
this.CMD_DISP_ROT,
|
||
rotation & 0xFF,
|
||
]);
|
||
}
|
||
|
||
async setDisplayIntensity(intensity) {
|
||
await this.sendKissCommand([
|
||
this.CMD_DISP_INT,
|
||
intensity & 0xFF,
|
||
]);
|
||
}
|
||
|
||
async setDisplayTimeout(timeout) {
|
||
await this.sendKissCommand([
|
||
this.CMD_DISP_BLNK,
|
||
timeout & 0xFF,
|
||
]);
|
||
}
|
||
|
||
async startDisplayReconditioning() {
|
||
await this.sendKissCommand([
|
||
this.CMD_DISP_RCND,
|
||
0x01,
|
||
]);
|
||
}
|
||
|
||
}
|
||
|
||
class ROM {
|
||
|
||
static PLATFORM_AVR = 0x90
|
||
static PLATFORM_ESP32 = 0x80
|
||
static PLATFORM_NRF52 = 0x70
|
||
|
||
static MCU_1284P = 0x91
|
||
static MCU_2560 = 0x92
|
||
static MCU_ESP32 = 0x81
|
||
static MCU_NRF52 = 0x71
|
||
|
||
static PRODUCT_RAK4631 = 0x10
|
||
static MODEL_11 = 0x11
|
||
static MODEL_12 = 0x12
|
||
|
||
static PRODUCT_RNODE = 0x03
|
||
static MODEL_A1 = 0xA1
|
||
static MODEL_A6 = 0xA6
|
||
static MODEL_A4 = 0xA4
|
||
static MODEL_A9 = 0xA9
|
||
static MODEL_A3 = 0xA3
|
||
static MODEL_A8 = 0xA8
|
||
static MODEL_A2 = 0xA2
|
||
static MODEL_A7 = 0xA7
|
||
static MODEL_A5 = 0xA5;
|
||
static MODEL_AA = 0xAA;
|
||
static MODEL_AC = 0xAC;
|
||
|
||
static PRODUCT_T32_10 = 0xB2
|
||
static MODEL_BA = 0xBA
|
||
static MODEL_BB = 0xBB
|
||
|
||
static PRODUCT_T32_20 = 0xB0
|
||
static MODEL_B3 = 0xB3
|
||
static MODEL_B8 = 0xB8
|
||
|
||
static PRODUCT_T32_21 = 0xB1
|
||
static MODEL_B4 = 0xB4
|
||
static MODEL_B9 = 0xB9
|
||
static MODEL_B4_TCXO = 0x04 // The TCXO model codes are only used here to select the
|
||
static MODEL_B9_TCXO = 0x09 // correct firmware, actual model codes in firmware is still 0xB4 and 0xB9.
|
||
|
||
static PRODUCT_H32_V2 = 0xC0
|
||
static MODEL_C4 = 0xC4
|
||
static MODEL_C9 = 0xC9
|
||
|
||
static PRODUCT_H32_V3 = 0xC1
|
||
static MODEL_C5 = 0xC5
|
||
static MODEL_CA = 0xCA
|
||
|
||
static PRODUCT_H32_V4 = 0xC3
|
||
static MODEL_C8 = 0xC8
|
||
|
||
static PRODUCT_HELTEC_T114 = 0xC2
|
||
static MODEL_C6 = 0xC6
|
||
static MODEL_C7 = 0xC7
|
||
|
||
static PRODUCT_TBEAM = 0xE0
|
||
static MODEL_E4 = 0xE4
|
||
static MODEL_E9 = 0xE9
|
||
static MODEL_E3 = 0xE3
|
||
static MODEL_E8 = 0xE8
|
||
|
||
static PRODUCT_TBEAM_S_V1 = 0xEA;
|
||
static PRODUCT_TBEAM_S_V3 = 0xEC;
|
||
static MODEL_DB = 0xDB
|
||
static MODEL_DC = 0xDC
|
||
|
||
static PRODUCT_TDECK = 0xD0;
|
||
static MODEL_D4 = 0xD4;
|
||
static MODEL_D9 = 0xD9;
|
||
|
||
static PRODUCT_TECHO = 0x15;
|
||
static MODEL_16 = 0x16;
|
||
static MODEL_17 = 0x17;
|
||
|
||
static PRODUCT_SENSECAP_T1000E = 0x24;
|
||
static MODEL_25 = 0x25;
|
||
|
||
static PRODUCT_XIAO_S3 = 0xEB;
|
||
static MODEL_DE = 0xDE;
|
||
static MODEL_DD = 0xDD;
|
||
|
||
static PRODUCT_HMBRW = 0xF0
|
||
static MODEL_FF = 0xFF
|
||
static MODEL_FE = 0xFE
|
||
|
||
static ADDR_PRODUCT = 0x00
|
||
static ADDR_MODEL = 0x01
|
||
static ADDR_HW_REV = 0x02
|
||
static ADDR_SERIAL = 0x03
|
||
static ADDR_MADE = 0x07
|
||
static ADDR_CHKSUM = 0x0B
|
||
static ADDR_SIGNATURE = 0x1B
|
||
static ADDR_INFO_LOCK = 0x9B
|
||
|
||
static ADDR_CONF_SF = 0x9C
|
||
static ADDR_CONF_CR = 0x9D
|
||
static ADDR_CONF_TXP = 0x9E
|
||
static ADDR_CONF_BW = 0x9F
|
||
static ADDR_CONF_FREQ = 0xA3
|
||
static ADDR_CONF_OK = 0xA7
|
||
static ADDR_CONF_BT = 0xB0
|
||
static ADDR_CONF_DSET = 0xB1
|
||
static ADDR_CONF_DINT = 0xB2
|
||
static ADDR_CONF_DADR = 0xB3
|
||
static ADDR_CONF_DBLK = 0xB4
|
||
static ADDR_CONF_DROT = 0xB8
|
||
static ADDR_CONF_PSET = 0xB5
|
||
static ADDR_CONF_PINT = 0xB6
|
||
static ADDR_CONF_BSET = 0xB7
|
||
static ADDR_CONF_DIA = 0xB9
|
||
static ADDR_CONF_WIFI = 0xBA
|
||
static ADDR_CONF_WCHN = 0xBB
|
||
static ADDR_CONF_SSID = 0x00
|
||
static ADDR_CONF_PSK = 0x21
|
||
static ADDR_CONF_IP = 0x42
|
||
static ADDR_CONF_NM = 0x46
|
||
|
||
static INFO_LOCK_BYTE = 0x73
|
||
static CONF_OK_BYTE = 0x73
|
||
|
||
static BOARD_RNODE = 0x31
|
||
static BOARD_HMBRW = 0x32
|
||
static BOARD_TBEAM = 0x33
|
||
static BOARD_HUZZAH32 = 0x34
|
||
static BOARD_GENERIC_ESP32 = 0x35
|
||
static BOARD_LORA32_V2_0 = 0x36
|
||
static BOARD_LORA32_V2_1 = 0x37
|
||
static BOARD_RAK4631 = 0x51
|
||
|
||
static MANUAL_FLASH_MODELS = [ROM.MODEL_A1, ROM.MODEL_A6]
|
||
|
||
constructor(eeprom) {
|
||
this.eeprom = eeprom;
|
||
}
|
||
|
||
getProduct() {
|
||
return this.eeprom[ROM.ADDR_PRODUCT];
|
||
}
|
||
|
||
getModel() {
|
||
return this.eeprom[ROM.ADDR_MODEL];
|
||
}
|
||
|
||
getHardwareRevision() {
|
||
return this.eeprom[ROM.ADDR_HW_REV];
|
||
}
|
||
|
||
getSerialNumber() {
|
||
return [
|
||
this.eeprom[ROM.ADDR_SERIAL],
|
||
this.eeprom[ROM.ADDR_SERIAL + 1],
|
||
this.eeprom[ROM.ADDR_SERIAL + 2],
|
||
this.eeprom[ROM.ADDR_SERIAL + 3],
|
||
];
|
||
}
|
||
|
||
getMade() {
|
||
return [
|
||
this.eeprom[ROM.ADDR_MADE],
|
||
this.eeprom[ROM.ADDR_MADE + 1],
|
||
this.eeprom[ROM.ADDR_MADE + 2],
|
||
this.eeprom[ROM.ADDR_MADE + 3],
|
||
];
|
||
}
|
||
|
||
getChecksum() {
|
||
const checksum = [];
|
||
for(var i = 0; i < 16; i++){
|
||
checksum.push(this.eeprom[ROM.ADDR_CHKSUM + i]);
|
||
}
|
||
return checksum;
|
||
}
|
||
|
||
getSignature() {
|
||
const signature = [];
|
||
for(var i = 0; i < 128; i++){
|
||
signature.push(this.eeprom[ROM.ADDR_SIGNATURE + i]);
|
||
}
|
||
return signature;
|
||
}
|
||
|
||
getCalculatedChecksum() {
|
||
return Utils.md5([
|
||
this.getProduct(),
|
||
this.getModel(),
|
||
this.getHardwareRevision(),
|
||
...this.getSerialNumber(),
|
||
...this.getMade(),
|
||
]);
|
||
}
|
||
|
||
getConfiguredSpreadingFactor() {
|
||
return this.eeprom[ROM.ADDR_CONF_SF];
|
||
}
|
||
|
||
getConfiguredCodingRate() {
|
||
return this.eeprom[ROM.ADDR_CONF_CR];
|
||
}
|
||
|
||
getConfiguredTxPower() {
|
||
return this.eeprom[ROM.ADDR_CONF_TXP];
|
||
}
|
||
|
||
getConfiguredFrequency() {
|
||
return this.eeprom[ROM.ADDR_CONF_FREQ] << 24
|
||
| this.eeprom[ROM.ADDR_CONF_FREQ + 1] << 16
|
||
| this.eeprom[ROM.ADDR_CONF_FREQ + 2] << 8
|
||
| this.eeprom[ROM.ADDR_CONF_FREQ + 3];
|
||
}
|
||
|
||
getConfiguredBandwidth() {
|
||
return this.eeprom[ROM.ADDR_CONF_BW] << 24
|
||
| this.eeprom[ROM.ADDR_CONF_BW + 1] << 16
|
||
| this.eeprom[ROM.ADDR_CONF_BW + 2] << 8
|
||
| this.eeprom[ROM.ADDR_CONF_BW + 3];
|
||
}
|
||
|
||
isInfoLocked() {
|
||
return this.eeprom[ROM.ADDR_INFO_LOCK] === ROM.INFO_LOCK_BYTE;
|
||
}
|
||
|
||
isConfigured() {
|
||
return this.eeprom[ROM.ADDR_CONF_OK] === ROM.CONF_OK_BYTE;
|
||
}
|
||
|
||
parse() {
|
||
|
||
// ensure info lock byte is set
|
||
if(!this.isInfoLocked()){
|
||
return null;
|
||
}
|
||
|
||
// convert to hex
|
||
const checksumHex = Utils.bytesToHex(this.getChecksum());
|
||
const calculatedChecksumHex = Utils.bytesToHex(this.getCalculatedChecksum());
|
||
const signatureHex = Utils.bytesToHex(this.getSignature());
|
||
|
||
// add details
|
||
var details = {
|
||
is_provisioned: true,
|
||
is_configured: this.isConfigured(),
|
||
product: this.getProduct(),
|
||
model: this.getModel(),
|
||
hardware_revision: this.getHardwareRevision(),
|
||
serial_number: Utils.unpackUInt32BE(this.getSerialNumber()),
|
||
made: Utils.unpackUInt32BE(this.getMade()),
|
||
checksum: checksumHex,
|
||
calculated_checksum: calculatedChecksumHex,
|
||
signature: signatureHex,
|
||
}
|
||
|
||
// if configured, add configuration to details
|
||
if(details.is_configured){
|
||
details = {
|
||
...details,
|
||
configured_spreading_factor: this.getConfiguredSpreadingFactor(),
|
||
configured_coding_rate: this.getConfiguredCodingRate(),
|
||
configured_tx_power: this.getConfiguredTxPower(),
|
||
configured_frequency: this.getConfiguredFrequency(),
|
||
configured_bandwidth: this.getConfiguredBandwidth(),
|
||
};
|
||
}
|
||
|
||
// if checksum in eeprom does not match checksum calculated from info, it is not provisioned
|
||
if(details.checksum !== details.calculated_checksum){
|
||
details.is_provisioned = false;
|
||
}
|
||
|
||
return details;
|
||
|
||
}
|
||
|
||
}
|