mirror of
https://github.com/meshcore-dev/meshcore.js.git
synced 2026-04-20 22:13:49 +00:00
Merge 195983d42a into 9a979df471
This commit is contained in:
commit
90eda01174
7 changed files with 333 additions and 18 deletions
14
.github/workflows/test.yml
vendored
Normal file
14
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
name: Run tests
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "node --test test/"
|
||||
},
|
||||
"author": "Liam Cottle <liam@liamcottle.com>",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,9 @@
|
|||
import BufferReader from "./buffer_reader.js";
|
||||
import BufferWriter from "./buffer_writer.js";
|
||||
import Constants from "./constants.js";
|
||||
|
||||
class Advert {
|
||||
|
||||
static ADV_TYPE_NONE = 0;
|
||||
static ADV_TYPE_CHAT = 1;
|
||||
static ADV_TYPE_REPEATER = 2;
|
||||
static ADV_TYPE_ROOM = 3;
|
||||
|
||||
static ADV_LATLON_MASK = 0x10;
|
||||
static ADV_BATTERY_MASK = 0x20;
|
||||
static ADV_TEMPERATURE_MASK = 0x40;
|
||||
static ADV_NAME_MASK = 0x80;
|
||||
|
||||
constructor(publicKey, timestamp, signature, appData) {
|
||||
this.publicKey = publicKey;
|
||||
this.timestamp = timestamp;
|
||||
|
|
@ -45,10 +36,10 @@ class Advert {
|
|||
|
||||
getTypeString() {
|
||||
const type = this.getType();
|
||||
if(type === Advert.ADV_TYPE_NONE) return "NONE";
|
||||
if(type === Advert.ADV_TYPE_CHAT) return "CHAT";
|
||||
if(type === Advert.ADV_TYPE_REPEATER) return "REPEATER";
|
||||
if(type === Advert.ADV_TYPE_ROOM) return "ROOM";
|
||||
if(type === Constants.AdvType.None) return "NONE";
|
||||
if(type === Constants.AdvType.Chat) return "CHAT";
|
||||
if(type === Constants.AdvType.Repeater) return "REPEATER";
|
||||
if(type === Constants.AdvType.Room) return "ROOM";
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -76,14 +67,14 @@ class Advert {
|
|||
// parse lat lon
|
||||
var lat = null;
|
||||
var lon = null;
|
||||
if(flags & Advert.ADV_LATLON_MASK){
|
||||
if(flags & Constants.AdvertFlags.LatLon){
|
||||
lat = bufferReader.readInt32LE();
|
||||
lon = bufferReader.readInt32LE();
|
||||
}
|
||||
|
||||
// parse name (remainder of app data)
|
||||
var name = null;
|
||||
if(flags & Advert.ADV_NAME_MASK){
|
||||
if(flags & Constants.AdvertFlags.Name){
|
||||
name = bufferReader.readString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ class BufferWriter {
|
|||
this.writeBytes(bytes);
|
||||
}
|
||||
|
||||
// not little-endian because a single byte has no ordering
|
||||
writeInt8(num) {
|
||||
const bytes = new Uint8Array(1);
|
||||
const view = new DataView(bytes.buffer);
|
||||
view.setInt8(0, num);
|
||||
this.writeBytes(bytes);
|
||||
}
|
||||
|
||||
writeString(string) {
|
||||
this.writeBytes(new TextEncoder().encode(string));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ class Connection extends EventEmitter {
|
|||
data.writeBytes(publicKey);
|
||||
data.writeByte(type);
|
||||
data.writeByte(flags);
|
||||
data.writeByte(outPathLen); // todo writeInt8
|
||||
data.writeInt8(outPathLen);
|
||||
data.writeBytes(outPath); // 64 bytes
|
||||
data.writeCString(advName, 32); // 32 bytes
|
||||
data.writeUInt32LE(lastAdvert);
|
||||
|
|
|
|||
|
|
@ -126,6 +126,13 @@ class Constants {
|
|||
GetNeighbours: 0x06, // #define REQ_TYPE_GET_NEIGHBOURS 0x06
|
||||
}
|
||||
|
||||
static AdvertFlags = {
|
||||
LatLon: 0x10,
|
||||
Battery: 0x20,
|
||||
Temperature: 0x40,
|
||||
Name: 0x80,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Constants;
|
||||
|
|
|
|||
295
test/protocol.test.js
Normal file
295
test/protocol.test.js
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import Connection from '../src/connection/connection.js';
|
||||
import BufferWriter from '../src/buffer_writer.js';
|
||||
import Constants from '../src/constants.js';
|
||||
|
||||
// Helper to wait for an event from the connection
|
||||
function waitForEvent(conn, eventCode) {
|
||||
return new Promise((resolve) => {
|
||||
conn.on(eventCode, (data) => {
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('SelfInfo Response Parsing', () => {
|
||||
it('should parse SelfInfo with correct field order', async () => {
|
||||
// Build a mock SelfInfo response
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.ResponseCodes.SelfInfo);
|
||||
writer.writeByte(Constants.AdvType.Chat); // type
|
||||
writer.writeByte(20); // txPower
|
||||
writer.writeByte(30); // maxTxPower
|
||||
writer.writeBytes(new Uint8Array(32).fill(0xAB)); // publicKey
|
||||
writer.writeInt32LE(12345678); // advLat
|
||||
writer.writeInt32LE(-87654321); // advLon
|
||||
writer.writeBytes(new Uint8Array(3)); // reserved
|
||||
writer.writeByte(1); // manualAddContacts
|
||||
writer.writeUInt32LE(915000); // radioFreq
|
||||
writer.writeUInt32LE(125); // radioBw
|
||||
writer.writeByte(10); // radioSf
|
||||
writer.writeByte(5); // radioCr
|
||||
writer.writeString('TestNode'); // name
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.ResponseCodes.SelfInfo);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.strictEqual(result.type, 1);
|
||||
assert.strictEqual(result.txPower, 20);
|
||||
assert.strictEqual(result.maxTxPower, 30);
|
||||
assert.deepStrictEqual(result.publicKey, new Uint8Array(32).fill(0xAB));
|
||||
assert.strictEqual(result.advLat, 12345678);
|
||||
assert.strictEqual(result.advLon, -87654321);
|
||||
assert.strictEqual(result.manualAddContacts, 1);
|
||||
assert.strictEqual(result.radioFreq, 915000);
|
||||
assert.strictEqual(result.radioBw, 125);
|
||||
assert.strictEqual(result.radioSf, 10);
|
||||
assert.strictEqual(result.radioCr, 5);
|
||||
assert.strictEqual(result.name, 'TestNode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Contact Response Parsing', () => {
|
||||
it('should parse Contact with correct field order starting with publicKey', async () => {
|
||||
// Create a specific outPath with known values for the first 3 bytes (matching outPathLen)
|
||||
const outPath = new Uint8Array(64).fill(0x00);
|
||||
outPath[0] = 0xAA; // First hop
|
||||
outPath[1] = 0xBB; // Second hop
|
||||
outPath[2] = 0xCC; // Third hop
|
||||
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.ResponseCodes.Contact);
|
||||
writer.writeBytes(new Uint8Array(32).fill(0xCD)); // publicKey
|
||||
writer.writeByte(Constants.AdvType.Repeater); // type
|
||||
writer.writeByte(0x01); // flags
|
||||
writer.writeInt8(3); // outPathLen
|
||||
writer.writeBytes(outPath); // outPath (fixed 64 bytes)
|
||||
writer.writeCString('James Example', 32); // advName (32 bytes C-string)
|
||||
writer.writeUInt32LE(1704067200); // lastAdvert (timestamp)
|
||||
writer.writeUInt32LE(40000000); // advLat
|
||||
writer.writeUInt32LE(-74000000 >>> 0); // advLon (as unsigned)
|
||||
writer.writeUInt32LE(1704153600); // lastMod
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.ResponseCodes.Contact);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.deepStrictEqual(result.publicKey, new Uint8Array(32).fill(0xCD));
|
||||
assert.strictEqual(result.type, 2);
|
||||
assert.strictEqual(result.flags, 0x01);
|
||||
assert.strictEqual(result.outPathLen, 3);
|
||||
assert.strictEqual(result.outPath.length, 64);
|
||||
// Verify the actual path bytes match what was written
|
||||
assert.strictEqual(result.outPath[0], 0xAA);
|
||||
assert.strictEqual(result.outPath[1], 0xBB);
|
||||
assert.strictEqual(result.outPath[2], 0xCC);
|
||||
// Note: bytes beyond outPathLen are undefined and should not be asserted
|
||||
assert.strictEqual(result.advName, 'James Example');
|
||||
assert.strictEqual(result.lastAdvert, 1704067200);
|
||||
});
|
||||
|
||||
it('should parse Contact with empty outPath (direct connection)', async () => {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.ResponseCodes.Contact);
|
||||
writer.writeBytes(new Uint8Array(32).fill(0xEE)); // publicKey
|
||||
writer.writeByte(Constants.AdvType.Chat); // type
|
||||
writer.writeByte(0x00); // flags
|
||||
writer.writeInt8(0); // outPathLen = 0 (direct connection)
|
||||
writer.writeBytes(new Uint8Array(64).fill(0x00)); // outPath (all zeros)
|
||||
writer.writeCString('Direct Contact', 32); // advName
|
||||
writer.writeUInt32LE(1704067200); // lastAdvert
|
||||
writer.writeUInt32LE(0); // advLat
|
||||
writer.writeUInt32LE(0); // advLon
|
||||
writer.writeUInt32LE(1704153600); // lastMod
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.ResponseCodes.Contact);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.strictEqual(result.outPathLen, 0);
|
||||
assert.strictEqual(result.outPath.length, 64);
|
||||
// Note: when outPathLen is 0, all bytes in outPath are undefined and should not be asserted
|
||||
});
|
||||
|
||||
it('should parse Contact with longer multi-hop outPath', async () => {
|
||||
// Create a path with 6 hops
|
||||
const outPath = new Uint8Array(64).fill(0x00);
|
||||
const pathHops = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
|
||||
for (let i = 0; i < pathHops.length; i++) {
|
||||
outPath[i] = pathHops[i];
|
||||
}
|
||||
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.ResponseCodes.Contact);
|
||||
writer.writeBytes(new Uint8Array(32).fill(0xAB)); // publicKey
|
||||
writer.writeByte(Constants.AdvType.Repeater); // type
|
||||
writer.writeByte(0x03); // flags
|
||||
writer.writeInt8(6); // outPathLen = 6 hops
|
||||
writer.writeBytes(outPath); // outPath
|
||||
writer.writeCString('Multi-hop Node', 32); // advName
|
||||
writer.writeUInt32LE(1704067200); // lastAdvert
|
||||
writer.writeUInt32LE(40000000); // advLat
|
||||
writer.writeUInt32LE(0); // advLon
|
||||
writer.writeUInt32LE(1704153600); // lastMod
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.ResponseCodes.Contact);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.strictEqual(result.outPathLen, 6);
|
||||
assert.strictEqual(result.outPath.length, 64);
|
||||
// Verify all path hops are correctly parsed
|
||||
for (let i = 0; i < pathHops.length; i++) {
|
||||
assert.strictEqual(result.outPath[i], pathHops[i], `outPath[${i}] should be 0x${pathHops[i].toString(16)}`);
|
||||
}
|
||||
// Note: bytes beyond outPathLen are undefined and should not be asserted
|
||||
});
|
||||
});
|
||||
|
||||
describe('LogRxData Push Parsing', () => {
|
||||
it('should parse SNR and RSSI from beginning of payload', async () => {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.PushCodes.LogRxData);
|
||||
writer.writeInt8(40); // snr * 4 = 10.0
|
||||
writer.writeInt8(-90); // rssi
|
||||
writer.writeBytes([0xDE, 0xAD, 0xBE, 0xEF]); // raw payload
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.PushCodes.LogRxData);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.strictEqual(result.lastSnr, 10.0); // 40/4
|
||||
assert.strictEqual(result.lastRssi, -90);
|
||||
assert.deepStrictEqual(result.raw, new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]));
|
||||
});
|
||||
|
||||
it('should handle negative SNR values', async () => {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.PushCodes.LogRxData);
|
||||
writer.writeInt8(-20); // snr * 4 = -5.0
|
||||
writer.writeInt8(-110); // rssi
|
||||
writer.writeBytes([0x01, 0x02]); // raw payload
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.PushCodes.LogRxData);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.strictEqual(result.lastSnr, -5.0); // -20/4
|
||||
assert.strictEqual(result.lastRssi, -110);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SendConfirmed Push Parsing', () => {
|
||||
it('should parse ackCode as 4 bytes (uint32)', async () => {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.PushCodes.SendConfirmed);
|
||||
writer.writeUInt32LE(0xDEADBEEF); // ackCode (4 bytes)
|
||||
writer.writeUInt32LE(1500); // roundTrip (4 bytes)
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.PushCodes.SendConfirmed);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.strictEqual(result.ackCode, 0xDEADBEEF);
|
||||
assert.strictEqual(result.roundTrip, 1500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RawData Push Parsing', () => {
|
||||
it('should parse SNR, RSSI, reserved byte, then payload', async () => {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.PushCodes.RawData);
|
||||
writer.writeInt8(24); // snr * 4 = 6.0
|
||||
writer.writeInt8(-85); // rssi
|
||||
writer.writeByte(0x00); // reserved
|
||||
writer.writeBytes([0xCA, 0xFE, 0xBA, 0xBE]); // payload
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.PushCodes.RawData);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.strictEqual(result.lastSnr, 6.0); // 24/4
|
||||
assert.strictEqual(result.lastRssi, -85);
|
||||
assert.deepStrictEqual(result.payload, new Uint8Array([0xCA, 0xFE, 0xBA, 0xBE]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('NewAdvert Push Parsing', () => {
|
||||
it('should parse NewAdvert with lastMod field', async () => {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.PushCodes.NewAdvert);
|
||||
writer.writeBytes(new Uint8Array(32).fill(0xEF)); // publicKey
|
||||
writer.writeByte(Constants.AdvType.Chat); // type
|
||||
writer.writeByte(0x02); // flags
|
||||
writer.writeInt8(2); // outPathLen
|
||||
writer.writeBytes(new Uint8Array(64).fill(0x00)); // outPath (fixed 64 bytes)
|
||||
writer.writeCString('NewNode', 32); // advName (32 bytes C-string)
|
||||
writer.writeUInt32LE(1704067200); // lastAdvert
|
||||
writer.writeUInt32LE(40000000); // advLat
|
||||
writer.writeUInt32LE(0); // advLon
|
||||
writer.writeUInt32LE(1704153600); // lastMod
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.PushCodes.NewAdvert);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.deepStrictEqual(result.publicKey, new Uint8Array(32).fill(0xEF));
|
||||
assert.strictEqual(result.type, 1);
|
||||
assert.strictEqual(result.flags, 0x02);
|
||||
assert.strictEqual(result.outPathLen, 2);
|
||||
assert.strictEqual(result.outPath.length, 64);
|
||||
assert.strictEqual(result.advName, 'NewNode');
|
||||
assert.strictEqual(result.lastAdvert, 1704067200);
|
||||
assert.strictEqual(result.lastMod, 1704153600);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sent Response Parsing', () => {
|
||||
it('should parse Sent response with result, expectedAckCrc, and estTimeout', async () => {
|
||||
const writer = new BufferWriter();
|
||||
writer.writeByte(Constants.ResponseCodes.Sent);
|
||||
writer.writeInt8(0); // result
|
||||
writer.writeUInt32LE(0x12345678); // expectedAckCrc
|
||||
writer.writeUInt32LE(5000); // estTimeout
|
||||
|
||||
const conn = new Connection();
|
||||
const resultPromise = waitForEvent(conn, Constants.ResponseCodes.Sent);
|
||||
|
||||
conn.onFrameReceived(writer.toBytes());
|
||||
|
||||
const result = await resultPromise;
|
||||
|
||||
assert.strictEqual(result.result, 0);
|
||||
assert.strictEqual(result.expectedAckCrc, 0x12345678);
|
||||
assert.strictEqual(result.estTimeout, 5000);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue