diff --git a/lora/.npmrc b/lora/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/lora/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/lora/LoraStream.js b/lora/LoraStream.js new file mode 100644 index 0000000..110bfe2 --- /dev/null +++ b/lora/LoraStream.js @@ -0,0 +1,60 @@ +import { Transform } from 'stream'; + +export class LoraStream extends Transform { + constructor(options) { + super(options); + this.byteBuffer = new Uint8Array([]); + this.textDecoder = new TextDecoder(); + } + + _transform(chunk, encoding, callback) { + this.byteBuffer = new Uint8Array([ + ...this.byteBuffer, + ...chunk, + ]); + let processingExhausted = false; + while (this.byteBuffer.length !== 0 && !processingExhausted) { + const framingIndex = this.byteBuffer.findIndex((byte) => byte === 0x94); + const framingByte2 = this.byteBuffer[framingIndex + 1]; + if (framingByte2 === 0xc3) { + if (this.byteBuffer.subarray(0, framingIndex).length) { + this.byteBuffer = this.byteBuffer.subarray(framingIndex); + } + const msb = this.byteBuffer[2]; + const lsb = this.byteBuffer[3]; + if (msb !== undefined && lsb !== undefined && this.byteBuffer.length >= 4 + (msb << 8) + lsb) { + const packet = this.byteBuffer.subarray(4, 4 + (msb << 8) + lsb); + + const malformedDetectorIndex = packet.findIndex( + (byte) => byte === 0x94, + ); + if (malformedDetectorIndex !== -1 && packet[malformedDetectorIndex + 1] === 0xc3) { + // malformed + this.byteBuffer = this.byteBuffer.subarray(malformedDetectorIndex); + } else { + this.byteBuffer = this.byteBuffer.subarray(3 + (msb << 8) + lsb + 1); + this.push(packet); + } + } else { + /** Only partioal message in buffer, wait for the rest */ + processingExhausted = true; + } + } else { + /** Message not complete, only 1 byte in buffer */ + processingExhausted = true; + } + } + + callback(); + } + _flush(callback) { + try { + if (this._buffer) { + this.push(this._buffer.trim()); + } + callback(); + } catch (err) { + callback(err); + } + } +} \ No newline at end of file diff --git a/lora/MeshtasticStream.js b/lora/MeshtasticStream.js new file mode 100644 index 0000000..9f0859d --- /dev/null +++ b/lora/MeshtasticStream.js @@ -0,0 +1,61 @@ +import { Transform } from 'stream'; +import { Mesh, Channel, Config, ModuleConfig } from '@meshtastic/protobufs'; +import { fromBinary, create } from '@bufbuild/protobuf'; + +export class MeshtasticStream extends Transform { + constructor(options) { + super({ readableObjectMode: true, ...options }); + } + _transform(chunk, encoding, callback) { + const dataPacket = fromBinary(Mesh.FromRadioSchema, chunk); + let schema = null; + switch(dataPacket.payloadVariant.case) { + case 'packet': + schema = Mesh.MeshPacketSchema; + break; + case 'nodeInfo': + schema = Mesh.NodeInfoSchema; + break; + case 'myInfo': + schema = Mesh.MyNodeInfoSchema; + break; + case 'deviceuiConfig': + // can't find, come back to this + break; + case 'config': + // todo: config has another payloadVariant within it + schema = Config.ConfigSchema + break; + case 'moduleConfig': + // todo: config has another payloadVariant within it + schema = ModuleConfig.ModuleConfigSchema + break; + case 'fileInfo': + schema = Mesh.FileInfoSchema + break; + case 'channel': + schema = Channel.ChannelSchema + break; + case 'metadata': + schema = Mesh.DeviceMetadataSchema + break; + case 'configCompleteId': + // done sending init data + break; + } + if (schema !== null) { + this.push(create(schema, dataPacket.payloadVariant.value)); + } + callback(); + } + _flush(callback) { + try { + if (this._buffer) { + this.push(this._buffer.trim()); + } + callback(); + } catch (err) { + callback(err); + } + } +} \ No newline at end of file diff --git a/lora/index.js b/lora/index.js new file mode 100644 index 0000000..aa38242 --- /dev/null +++ b/lora/index.js @@ -0,0 +1,103 @@ +const { Mesh, Mqtt, Portnums, Telemetry, Config, Channel } = require("@meshtastic/protobufs"); +const { fromBinary, toBinary, create } = require("@bufbuild/protobuf"); +const { LoraStream } = require("./LoraStream"); +const { MeshtasticStream } = require("./MeshtasticStream"); + +const net = require('net'); + +const client = new net.Socket(); +const IP = '192.168.10.117'; +const PORT = 4403; + +function sendHello() { + const data = create(Mesh.ToRadioSchema, { + payloadVariant: { + case: "wantConfigId", + value: 1, + } + }); + sendToDevice(toBinary(Mesh.ToRadioSchema, data)); +} + +function sendToDevice(data) { + const bufferLength = data.length; + const header = new Uint8Array([ + 0x94, + 0xC3, + (bufferLength >> 8) & 0xFF, + bufferLength & 0xFF, + ]); + data = new Uint8Array([...header, ...data]); + client.write(data); +} + +client.connect(PORT, IP, function() { + console.log('Connected'); + sendHello(); +}); + +const meshtasticStream = new MeshtasticStream(); +client.pipe(new LoraStream()).pipe(meshtasticStream); +meshtasticStream.on('data', (data) => { + parseMeshtastic(data['$typeName'], data); +}) + +function parseMeshtastic(typeName, data) { + switch(typeName) { + case Mesh.MeshPacketSchema.typeName: + onMeshPacket(data); + break; + case Mesh.NodeInfoSchema.typeName: + console.log('node info'); + break; + } +} + +function onMeshPacket(envelope) { + const payloadVariant = envelope.payloadVariant.case; + + if (payloadVariant === 'encrypted') { + // attempt decryption + } + + const dataPacket = envelope.payloadVariant.value; + const portNum = dataPacket.portnum; + + if (!portNum) { + return; + } + + let schema = null; + switch (portNum) { + case Portnums.PortNum.POSITION_APP: + schema = Mesh.PositionSchema; + break; + case Portnums.PortNum.TELEMETRY_APP: + schema = Telemetry.TelemetrySchema; + break; + case Portnums.PortNum.TEXT_MESSAGE_APP: + // no schema? + break; + case Portnums.PortNum.WAYPOINT_APP: + schema = Mesh.WaypointSchema; + break; + case Portnums.PortNum.TRACEROUTE_APP: + schema = Mesh.RouteDiscoverySchema; + break; + case Portnums.PortNum.NODEINFO_APP: + schema = Mesh.NodeInfoSchema; + break; + case Portnums.PortNum.NEIGHBORINFO_APP: + schema = Mesh.NeighborInfoSchema; + break; + case Portnums.PortNum.MAP_REPORT_APP: + schema = Mqtt.MapReportSchema; + break; + } + + if (schema !== null) { + console.log(dataPacket); + const decodedData = fromBinary(schema, dataPacket.payload); + console.log(decodedData); + } +} \ No newline at end of file diff --git a/lora/package-lock.json b/lora/package-lock.json new file mode 100644 index 0000000..c0b4d8b --- /dev/null +++ b/lora/package-lock.json @@ -0,0 +1,341 @@ +{ + "name": "lora", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lora", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@bufbuild/protobuf": "^2.2.5", + "@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2", + "@prisma/client": "^5.11.0", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.1" + }, + "devDependencies": { + "prisma": "^5.10.2" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz", + "integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@meshtastic/protobufs": { + "name": "@jsr/meshtastic__protobufs", + "version": "2.6.2", + "resolved": "https://npm.jsr.io/~/11/@jsr/meshtastic__protobufs/2.6.2.tgz", + "integrity": "sha512-bIENtFnUEru28GrAeSdiBS9skp0hN/3HZunMbF/IjvUrXOlx2fptKVj3b+pzjOWnLBZxllrByV/W+XDmrxqJ6g==", + "dependencies": { + "@bufbuild/protobuf": "^2.2.3" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrapjs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", + "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + } + } +} diff --git a/lora/package.json b/lora/package.json new file mode 100644 index 0000000..b8cfe5a --- /dev/null +++ b/lora/package.json @@ -0,0 +1,18 @@ +{ + "name": "lora", + "version": "1.0.0", + "main": "index.js", + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@bufbuild/protobuf": "^2.2.5", + "@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2", + "@prisma/client": "^5.11.0", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.1" + }, + "devDependencies": { + "prisma": "^5.10.2" + } +}