rewrite serial logic to handle out of order response payloads and fix kiss handling on windows

This commit is contained in:
liamcottle
2025-01-06 03:20:28 +13:00
parent d210505b87
commit 1007e2ebd5

View File

@@ -121,8 +121,10 @@ class RNode {
constructor(serialPort) { constructor(serialPort) {
this.serialPort = serialPort; this.serialPort = serialPort;
this.readable = serialPort.readable; this.reader = serialPort.readable.getReader();
this.writable = serialPort.writable; this.writable = serialPort.writable;
this.callbacks = {};
this.readLoop();
} }
static async fromSerialPort(serialPort) { static async fromSerialPort(serialPort) {
@@ -137,11 +139,21 @@ class RNode {
} }
async close() { 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 { try {
await this.serialPort.close(); await this.serialPort.close();
} catch(e) { } catch(e) {
console.log("failed to close serial port, ignoring...", e); console.log("failed to close serial port, ignoring...", e);
} }
} }
async write(bytes) { async write(bytes) {
@@ -153,79 +165,100 @@ class RNode {
} }
} }
async readFromSerialPort(timeoutMillis) { async readLoop() {
return new Promise(async (resolve, reject) => { try {
let buffer = [];
let inFrame = false;
while(true){
// create reader // read kiss frames until reader indicates it's done
const reader = this.readable.getReader(); const { value, done } = await this.reader.read();
if(done){
break;
}
// timeout after provided millis // read kiss frames
if(timeoutMillis != null){ for(const byte of value){
setTimeout(() => { if(byte === this.KISS_FEND){
reader.releaseLock(); if(inFrame){
reject("timeout"); // End of frame
}, timeoutMillis); const decodedFrame = this.decodeKissFrame(buffer);
} if(decodedFrame){
this.onCommandReceived(decodedFrame);
// attempt to read kiss frame } else {
try { console.warn("Invalid frame ignored.");
let buffer = [];
while(true){
const { value, done } = await reader.read();
if(done){
break;
}
if(value){
for(let byte of value){
buffer.push(byte);
if(byte === this.KISS_FEND){
if(buffer.length > 1){
resolve(this.handleKISSFrame(buffer));
return;
}
buffer = [this.KISS_FEND]; // Start new frame
} }
buffer = [];
} }
inFrame = !inFrame;
} else if(inFrame) {
buffer.push(byte);
} }
} }
} catch (error) {
console.error('Error reading from serial port: ', error); }
} finally { } catch(error) {
reader.releaseLock();
// ignore error if reader was released
if(error instanceof TypeError){
return;
} }
}); console.error('Error reading from serial port: ', error);
} finally {
this.reader.releaseLock();
}
} }
handleKISSFrame(frame) { onCommandReceived(data) {
try {
let data = []; // 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; let escaping = false;
// Skip the initial 0xC0 and process the rest for(const byte of frame){
for(let i = 1; i < frame.length; i++){ if(escaping){
let byte = frame[i]; if(byte === this.KISS_TFEND){
if (escaping) {
if (byte === this.KISS_TFEND) {
data.push(this.KISS_FEND); data.push(this.KISS_FEND);
} else if (byte === this.KISS_TFESC) { } else if(byte === this.KISS_TFESC) {
data.push(this.KISS_FESC); data.push(this.KISS_FESC);
} else {
return null; // Invalid escape sequence
} }
escaping = false; escaping = false;
} else if(byte === this.KISS_FESC) {
escaping = true;
} else { } else {
if (byte === this.KISS_FESC) { data.push(byte);
escaping = true;
} else if (byte === this.KISS_FEND) {
// Ignore the end frame delimiter
break;
} else {
data.push(byte);
}
} }
} }
//console.log('Received KISS frame data:', new Uint8Array(data)); // return null if incomplete escape at end
return data; return escaping ? null : data;
} }
@@ -248,6 +281,28 @@ class RNode {
await this.write(this.createKissFrame(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() { async reset() {
await this.sendKissCommand([ await this.sendKissCommand([
this.CMD_RESET, this.CMD_RESET,
@@ -256,30 +311,42 @@ class RNode {
} }
async detect() { async detect() {
return new Promise(async (resolve) => {
try {
// ask if device is rnode // timeout after provided millis
await this.sendKissCommand([ const timeout = setTimeout(() => {
this.CMD_DETECT, resolve(false);
this.DETECT_REQ, }, 2000);
]);
// read response from device // detect rnode
const [ command, responseByte ] = await this.readFromSerialPort(); const response = await this.sendCommand(this.CMD_DETECT, [
this.DETECT_REQ,
]);
// device is an rnode if response is as expected // we no longer want to timeout
return command === this.CMD_DETECT && responseByte === this.DETECT_RESP; 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() { async getFirmwareVersion() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_FW_VERSION, [
this.CMD_FW_VERSION,
0x00, 0x00,
]); ]);
// read response from device // read response from device
var [ command, majorVersion, minorVersion ] = await this.readFromSerialPort(); var [ majorVersion, minorVersion ] = response;
if(minorVersion.length === 1){ if(minorVersion.length === 1){
minorVersion = "0" + minorVersion; minorVersion = "0" + minorVersion;
} }
@@ -291,99 +358,91 @@ class RNode {
async getPlatform() { async getPlatform() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_PLATFORM, [
this.CMD_PLATFORM,
0x00, 0x00,
]); ]);
// read response from device // read response from device
const [ command, platformByte ] = await this.readFromSerialPort(); const [ platformByte ] = response;
return platformByte; return platformByte;
} }
async getMcu() { async getMcu() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_MCU, [
this.CMD_MCU,
0x00, 0x00,
]); ]);
// read response from device // read response from device
const [ command, mcuByte ] = await this.readFromSerialPort(); const [ mcuByte ] = response;
return mcuByte; return mcuByte;
} }
async getBoard() { async getBoard() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_BOARD, [
this.CMD_BOARD,
0x00, 0x00,
]); ]);
// read response from device // read response from device
const [ command, boardByte ] = await this.readFromSerialPort(); const [ boardByte ] = response;
return boardByte; return boardByte;
} }
async getDeviceHash() { async getDeviceHash() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_DEV_HASH, [
this.CMD_DEV_HASH,
0x01, // anything != 0x00 0x01, // anything != 0x00
]); ]);
// read response from device // read response from device
const [ command, ...deviceHash ] = await this.readFromSerialPort(); const [ ...deviceHash ] = response;
return deviceHash; return deviceHash;
} }
async getTargetFirmwareHash() { async getTargetFirmwareHash() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_HASHES, [
this.CMD_HASHES,
this.HASH_TYPE_TARGET_FIRMWARE, this.HASH_TYPE_TARGET_FIRMWARE,
]); ]);
// read response from device // read response from device
const [ command, hashType, ...targetFirmwareHash ] = await this.readFromSerialPort(); const [ hashType, ...targetFirmwareHash ] = response;
return targetFirmwareHash; return targetFirmwareHash;
} }
async getFirmwareHash() { async getFirmwareHash() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_HASHES, [
this.CMD_HASHES,
this.HASH_TYPE_FIRMWARE, this.HASH_TYPE_FIRMWARE,
]); ]);
// read response from device // read response from device
const [ command, hashType, ...firmwareHash ] = await this.readFromSerialPort(); const [ hashType, ...firmwareHash ] = response;
return firmwareHash; return firmwareHash;
} }
async getRom() { async getRom() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_ROM_READ, [
this.CMD_ROM_READ,
0x00, 0x00,
]); ]);
// read response from device // read response from device
const [ command, ...eepromBytes ] = await this.readFromSerialPort(); const [ ...eepromBytes ] = response;
return eepromBytes; return eepromBytes;
} }
async getFrequency() { async getFrequency() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_FREQUENCY, [
this.CMD_FREQUENCY,
// request frequency by sending zero as 4 bytes // request frequency by sending zero as 4 bytes
0x00, 0x00,
0x00, 0x00,
@@ -392,7 +451,7 @@ class RNode {
]); ]);
// read response from device // read response from device
const [ command, ...frequencyBytes ] = await this.readFromSerialPort(); const [ ...frequencyBytes ] = response;
// convert 4 bytes to 32bit integer representing frequency in hertz // convert 4 bytes to 32bit integer representing frequency in hertz
const frequencyInHz = frequencyBytes[0] << 24 | frequencyBytes[1] << 16 | frequencyBytes[2] << 8 | frequencyBytes[3]; const frequencyInHz = frequencyBytes[0] << 24 | frequencyBytes[1] << 16 | frequencyBytes[2] << 8 | frequencyBytes[3];
@@ -402,8 +461,7 @@ class RNode {
async getBandwidth() { async getBandwidth() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_BANDWIDTH, [
this.CMD_BANDWIDTH,
// request bandwidth by sending zero as 4 bytes // request bandwidth by sending zero as 4 bytes
0x00, 0x00,
0x00, 0x00,
@@ -412,7 +470,7 @@ class RNode {
]); ]);
// read response from device // read response from device
const [ command, ...bandwidthBytes ] = await this.readFromSerialPort(); const [ ...bandwidthBytes ] = response;
// convert 4 bytes to 32bit integer representing bandwidth in hertz // convert 4 bytes to 32bit integer representing bandwidth in hertz
const bandwidthInHz = bandwidthBytes[0] << 24 | bandwidthBytes[1] << 16 | bandwidthBytes[2] << 8 | bandwidthBytes[3]; const bandwidthInHz = bandwidthBytes[0] << 24 | bandwidthBytes[1] << 16 | bandwidthBytes[2] << 8 | bandwidthBytes[3];
@@ -422,69 +480,60 @@ class RNode {
async getTxPower() { async getTxPower() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_TXPOWER, [
this.CMD_TXPOWER,
0xFF, // request tx power 0xFF, // request tx power
]); ]);
// read response from device // read response from device
const [ command, txPower ] = await this.readFromSerialPort(); const [ txPower ] = response;
return txPower; return txPower;
} }
async getSpreadingFactor() { async getSpreadingFactor() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_SF, [
this.CMD_SF,
0xFF, // request spreading factor 0xFF, // request spreading factor
]); ]);
// read response from device // read response from device
const [ command, spreadingFactor ] = await this.readFromSerialPort(); const [ spreadingFactor ] = response;
return spreadingFactor; return spreadingFactor;
} }
async getCodingRate() { async getCodingRate() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_CR, [
this.CMD_CR,
0xFF, // request coding rate 0xFF, // request coding rate
]); ]);
// read response from device // read response from device
const [ command, codingRate ] = await this.readFromSerialPort(); const [ codingRate ] = response;
return codingRate; return codingRate;
} }
async getRadioState() { async getRadioState() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_RADIO_STATE, [
this.CMD_RADIO_STATE,
0xFF, // request radio state 0xFF, // request radio state
]); ]);
// read response from device // read response from device
const [ command, radioState ] = await this.readFromSerialPort(); const [ radioState ] = response;
return radioState; return radioState;
} }
async getRxStat() { async getRxStat() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_STAT_RX, [
this.CMD_STAT_RX,
0x00, 0x00,
]); ]);
// read response from device // read response from device
const [ command, ...statBytes ] = await this.readFromSerialPort(); const [ ...statBytes ] = response;
// convert 4 bytes to 32bit integer // convert 4 bytes to 32bit integer
const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3]; const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3];
@@ -494,13 +543,12 @@ class RNode {
async getTxStat() { async getTxStat() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_STAT_TX, [
this.CMD_STAT_TX,
0x00, 0x00,
]); ]);
// read response from device // read response from device
const [ command, ...statBytes ] = await this.readFromSerialPort(); const [ ...statBytes ] = response;
// convert 4 bytes to 32bit integer // convert 4 bytes to 32bit integer
const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3]; const stat = statBytes[0] << 24 | statBytes[1] << 16 | statBytes[2] << 8 | statBytes[3];
@@ -510,14 +558,12 @@ class RNode {
async getRssiStat() { async getRssiStat() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_STAT_RSSI, [
this.CMD_STAT_RSSI,
0x00, 0x00,
]); ]);
// read response from device // read response from device
const [ command, rssi ] = await this.readFromSerialPort(); const [ rssi ] = response;
return rssi; return rssi;
} }
@@ -536,7 +582,23 @@ class RNode {
]); ]);
} }
async startBluetoothPairing() { 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 // enable pairing
await this.sendKissCommand([ await this.sendKissCommand([
@@ -544,43 +606,16 @@ class RNode {
0x02, // enable pairing 0x02, // enable pairing
]); ]);
// todo: listen for packets, pin will be available once user has initiated pairing from Android device
// // attempt to get bluetooth pairing pin
// try {
//
// // read response from device
// const [ command, ...pinBytes ] = await this.readFromSerialPort(5000);
// if(command !== this.CMD_BT_PIN){
// throw `unexpected command response: ${command}`;
// }
//
// // convert 4 bytes to 32bit integer
// const pin = pinBytes[0] << 24 | pinBytes[1] << 16 | pinBytes[2] << 8 | pinBytes[3];
//
// // todo: remove logs
// console.log(pinBytes);
// console.log(pin);
//
// // todo: convert to string
// return pin;
//
// } catch(error) {
// throw `failed to get bluetooth pin: ${error}`;
// }
} }
async readDisplay() { async readDisplay() {
await this.sendKissCommand([ const response = await this.sendCommand(this.CMD_DISP_READ, [
this.CMD_DISP_READ,
0x01, 0x01,
]); ]);
// read response from device // read response from device
const [ command, ...displayBuffer ] = await this.readFromSerialPort(); const [ ...displayBuffer ] = response;
return displayBuffer; return displayBuffer;
} }