Compare commits
18 Commits
7b3e6e4fd1
...
master
Author | SHA1 | Date | |
---|---|---|---|
2f76091ae6 | |||
0f4035ec3a | |||
18df989fc6 | |||
e694ffa66a | |||
da99bfdeef | |||
b2a9488efd | |||
6b7149905a | |||
8497053aed | |||
bfdd6cba22 | |||
6554d270bc | |||
bcaa1f2c20 | |||
0a7e456173 | |||
d50fe75759 | |||
d963520486 | |||
e025140ab4 | |||
e8095fce81 | |||
af5690e524 | |||
33726de0cb |
@ -13,11 +13,11 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Build web app image
|
- name: Build web app image
|
||||||
run: docker build -f webapp/Dockerfile -t git.arinity.org/ctmesh/map:latest .
|
run: docker build --no-cache -f webapp/Dockerfile -t git.arinity.org/ctmesh/map:latest .
|
||||||
- name: Build mqtt listener image
|
- name: Build mqtt listener image
|
||||||
run: docker build -f mqtt/Dockerfile -t git.arinity.org/ctmesh/map-mqtt:latest .
|
run: docker build --no-cache -f mqtt/Dockerfile -t git.arinity.org/ctmesh/map-mqtt:latest .
|
||||||
- name: Build cli image
|
- name: Build cli image
|
||||||
run: docker build -f cli/Dockerfile -t git.arinity.org/ctmesh/map-cli:latest .
|
run: docker build --no-cache -f cli/Dockerfile -t git.arinity.org/ctmesh/map-cli:latest .
|
||||||
- name: Push images
|
- name: Push images
|
||||||
run: |
|
run: |
|
||||||
docker push git.arinity.org/ctmesh/map:latest
|
docker push git.arinity.org/ctmesh/map:latest
|
||||||
|
@ -6,7 +6,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
database:
|
database:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command: "--mqtt-topic=msh/US/#"
|
command: "--mqtt-broker-url= --mqtt-topic=msh/US/# --collect-service-envelopes --collect-neighbor-info --collect-waypoints"
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: "mysql://root:password@database:3306/meshtastic-map?connection_limit=100"
|
DATABASE_URL: "mysql://root:password@database:3306/meshtastic-map?connection_limit=100"
|
||||||
|
|
||||||
|
1
lora/.npmrc
Normal file
1
lora/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
@jsr:registry=https://npm.jsr.io
|
60
lora/LoraStream.js
Normal file
60
lora/LoraStream.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Transform } from 'stream';
|
||||||
|
|
||||||
|
export default 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
lora/MeshtasticStream.js
Normal file
62
lora/MeshtasticStream.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { Transform } from 'stream';
|
||||||
|
import { Mesh, Channel, Config, ModuleConfig } from '@meshtastic/protobufs';
|
||||||
|
import { fromBinary, create } from '@bufbuild/protobuf';
|
||||||
|
|
||||||
|
export default 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':
|
||||||
|
schema = Config.ConfigSchema
|
||||||
|
break;
|
||||||
|
case 'moduleConfig':
|
||||||
|
schema = ModuleConfig.ModuleConfigSchema
|
||||||
|
break;
|
||||||
|
case 'fileInfo':
|
||||||
|
schema = Mesh.FileInfoSchema
|
||||||
|
break;
|
||||||
|
case 'channel':
|
||||||
|
schema = Channel.ChannelSchema
|
||||||
|
break;
|
||||||
|
case 'metadata':
|
||||||
|
schema = Mesh.DeviceMetadataSchema
|
||||||
|
break;
|
||||||
|
case 'logRecord':
|
||||||
|
schema = Mesh.LogRecordSchema
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
lora/index.js
Normal file
107
lora/index.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { Mesh, Mqtt, Portnums, Telemetry, Config, Channel } from '@meshtastic/protobufs';
|
||||||
|
import { fromBinary, toBinary, create } from '@bufbuild/protobuf';
|
||||||
|
import { LoraStream } from './LoraStream';
|
||||||
|
import { MeshtasticStream } from './MeshtasticStream';
|
||||||
|
import net from '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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decodedData = dataPacket.payload;
|
||||||
|
if (schema !== null) {
|
||||||
|
try {
|
||||||
|
decodedData = fromBinary(schema, decodedData);
|
||||||
|
console.log(decodedData);
|
||||||
|
} catch(e) {
|
||||||
|
// ignore errors, likely incomplete data
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
341
lora/package-lock.json
generated
Normal file
341
lora/package-lock.json
generated
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
lora/package.json
Normal file
19
lora/package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "lora",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
612
mqtt/index.js
612
mqtt/index.js
@ -1,14 +1,13 @@
|
|||||||
const crypto = require("crypto");
|
import crypto from 'crypto';
|
||||||
const mqtt = require("mqtt");
|
import mqtt from "mqtt";
|
||||||
const commandLineArgs = require("command-line-args");
|
import commandLineArgs from 'command-line-args';
|
||||||
const commandLineUsage = require("command-line-usage");
|
import commandLineUsage from 'command-line-usage';
|
||||||
const PositionUtil = require("./utils/position_util");
|
import { fromBinary } from '@bufbuild/protobuf';
|
||||||
|
import { Mesh, Mqtt, Portnums, Telemetry } from '@meshtastic/protobufs';
|
||||||
const { Mesh, Mqtt, Portnums, Telemetry } = require("@meshtastic/protobufs");
|
import PositionUtil from './utils/position_util.js';
|
||||||
const { fromBinary } = require("@bufbuild/protobuf");
|
|
||||||
|
|
||||||
// create prisma db client
|
// create prisma db client
|
||||||
const { PrismaClient } = require("@prisma/client");
|
import { PrismaClient } from "@prisma/client";
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// meshtastic bitfield flags
|
// meshtastic bitfield flags
|
||||||
@ -87,9 +86,9 @@ const optionsList = [
|
|||||||
description: "This option will save all received waypoints to the database.",
|
description: "This option will save all received waypoints to the database.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "collect-neighbour-info",
|
name: "collect-neighbor-info",
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
description: "This option will save all received neighbour infos to the database.",
|
description: "This option will save all received neighbor infos to the database.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "collect-map-reports",
|
name: "collect-map-reports",
|
||||||
@ -108,6 +107,11 @@ const optionsList = [
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
description: "This option will drop all packets that have 'OK to MQTT' set to false.",
|
description: "This option will drop all packets that have 'OK to MQTT' set to false.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "debug-incoming-packets",
|
||||||
|
type: Boolean,
|
||||||
|
description: "This option will print out all known packets as they arrive.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "drop-portnums-without-bitfield",
|
name: "drop-portnums-without-bitfield",
|
||||||
type: Number,
|
type: Number,
|
||||||
@ -203,7 +207,7 @@ if(options.help){
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
console.log(usage);
|
console.log(usage);
|
||||||
return;
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get options and fallback to default values
|
// get options and fallback to default values
|
||||||
@ -214,16 +218,19 @@ const mqttClientId = options["mqtt-client-id"] ?? "mqttx_1bc723c7";
|
|||||||
const mqttTopics = options["mqtt-topic"] ?? ["msh/US/#"];
|
const mqttTopics = options["mqtt-topic"] ?? ["msh/US/#"];
|
||||||
const allowedPortnums = options["allowed-portnums"] ?? null;
|
const allowedPortnums = options["allowed-portnums"] ?? null;
|
||||||
const logUnknownPortnums = options["log-unknown-portnums"] ?? false;
|
const logUnknownPortnums = options["log-unknown-portnums"] ?? false;
|
||||||
const collectServiceEnvelopes = options["collect-service-envelopes"] ?? false;
|
const collectorEnabled = {
|
||||||
const collectPositions = options["collect-positions"] ?? false;
|
serviceEnvelopes: options["collect-service-envelopes"] ?? false,
|
||||||
const collectTextMessages = options["collect-text-messages"] ?? false;
|
positions: options["collect-positions"] ?? false,
|
||||||
const ignoreDirectMessages = options["ignore-direct-messages"] ?? false;
|
textMessages: options["collect-text-messages"] ?? false,
|
||||||
const collectWaypoints = options["collect-waypoints"] ?? false;
|
waypoints: options["collect-waypoints"] ?? false,
|
||||||
const collectNeighbourInfo = options["collect-neighbour-info"] ?? false;
|
mapReports: options["collect-map-reports"] ?? false,
|
||||||
const collectMapReports = options["collect-map-reports"] ?? false;
|
directMessages: !(options["ignore-direct-messages"] ?? false),
|
||||||
|
neighborInfo: options["collect-neighbor-info"] ?? false,
|
||||||
|
}
|
||||||
const decryptionKeys = options["decryption-keys"] ?? [
|
const decryptionKeys = options["decryption-keys"] ?? [
|
||||||
"1PG7OiApB1nwvP+rz05pAQ==", // add default "AQ==" decryption key
|
"1PG7OiApB1nwvP+rz05pAQ==", // add default "AQ==" decryption key
|
||||||
];
|
];
|
||||||
|
const logKnownPacketTypes = options["debug-incoming-packets"] ?? false;
|
||||||
const dropPacketsNotOkToMqtt = options["drop-packets-not-ok-to-mqtt"] ?? false;
|
const dropPacketsNotOkToMqtt = options["drop-packets-not-ok-to-mqtt"] ?? false;
|
||||||
const dropPortnumsWithoutBitfield = options["drop-portnums-without-bitfield"] ?? null;
|
const dropPortnumsWithoutBitfield = options["drop-portnums-without-bitfield"] ?? null;
|
||||||
const oldFirmwarePositionPrecision = options["old-firmware-position-precision"] ?? null;
|
const oldFirmwarePositionPrecision = options["old-firmware-position-precision"] ?? null;
|
||||||
@ -241,6 +248,7 @@ const purgeTextMessagesAfterSeconds = options["purge-text-messages-after-seconds
|
|||||||
const purgeTraceroutesAfterSeconds = options["purge-traceroutes-after-seconds"] ?? null;
|
const purgeTraceroutesAfterSeconds = options["purge-traceroutes-after-seconds"] ?? null;
|
||||||
const purgeWaypointsAfterSeconds = options["purge-waypoints-after-seconds"] ?? null;
|
const purgeWaypointsAfterSeconds = options["purge-waypoints-after-seconds"] ?? null;
|
||||||
|
|
||||||
|
|
||||||
// create mqtt client
|
// create mqtt client
|
||||||
const client = mqtt.connect(mqttBrokerUrl, {
|
const client = mqtt.connect(mqttBrokerUrl, {
|
||||||
username: mqttUsername,
|
username: mqttUsername,
|
||||||
@ -266,6 +274,15 @@ if(purgeIntervalSeconds){
|
|||||||
}, purgeIntervalSeconds * 1000);
|
}, purgeIntervalSeconds * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printStatus() {
|
||||||
|
console.log(`MQTT server: ${mqttBrokerUrl}`)
|
||||||
|
console.log(`MQTT topic(s): ${mqttTopics.join(', ')}`)
|
||||||
|
for (const key of Object.keys(collectorEnabled)) {
|
||||||
|
console.log(`${key} collection: ${collectorEnabled[key] ? 'enabled' : 'disabled'}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printStatus();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purges all nodes from the database that haven't been heard from within the configured timeframe.
|
* Purges all nodes from the database that haven't been heard from within the configured timeframe.
|
||||||
*/
|
*/
|
||||||
@ -666,14 +683,14 @@ function convertHexIdToNumericId(hexId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// subscribe to everything when connected
|
// subscribe to everything when connected
|
||||||
client.on("connect", () => {
|
client.on('connect', () => {
|
||||||
for(const mqttTopic of mqttTopics){
|
for(const mqttTopic of mqttTopics){
|
||||||
client.subscribe(mqttTopic);
|
client.subscribe(mqttTopic);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle message received
|
// handle message received
|
||||||
client.on("message", async (topic, message) => {
|
client.on('message', async (topic, message) => {
|
||||||
try {
|
try {
|
||||||
// decode service envelope
|
// decode service envelope
|
||||||
let envelope = null;
|
let envelope = null;
|
||||||
@ -687,7 +704,7 @@ client.on("message", async (topic, message) => {
|
|||||||
let dataPacket = envelope.packet.payloadVariant.value;
|
let dataPacket = envelope.packet.payloadVariant.value;
|
||||||
// attempt to decrypt encrypted packets
|
// attempt to decrypt encrypted packets
|
||||||
if (envelope.packet.payloadVariant.case === 'encrypted') {
|
if (envelope.packet.payloadVariant.case === 'encrypted') {
|
||||||
dataPacket = decrypt(envelope.packet);
|
envelope.packet.payloadVariant.value = dataPacket = decrypt(envelope.packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataPacket !== null) {
|
if (dataPacket !== null) {
|
||||||
@ -712,7 +729,7 @@ client.on("message", async (topic, message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create service envelope in db
|
// create service envelope in db
|
||||||
if(collectServiceEnvelopes){
|
if (collectorEnabled.serviceEnvelopes) {
|
||||||
try {
|
try {
|
||||||
await prisma.serviceEnvelope.create({
|
await prisma.serviceEnvelope.create({
|
||||||
data: {
|
data: {
|
||||||
@ -750,7 +767,7 @@ client.on("message", async (topic, message) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = dataPacket.payload;
|
let payload = dataPacket.payload;
|
||||||
// get portnum from decoded packet
|
// get portnum from decoded packet
|
||||||
const portnum = dataPacket.portnum;
|
const portnum = dataPacket.portnum;
|
||||||
// get bitfield from decoded packet
|
// get bitfield from decoded packet
|
||||||
@ -758,32 +775,100 @@ client.on("message", async (topic, message) => {
|
|||||||
// this value will be null for packets from v2.4.x and below, and will be an integer in v2.5.x and above
|
// this value will be null for packets from v2.4.x and below, and will be an integer in v2.5.x and above
|
||||||
const bitfield = dataPacket.bitfield;
|
const bitfield = dataPacket.bitfield;
|
||||||
|
|
||||||
const logKnownPacketTypes = false;
|
|
||||||
|
|
||||||
// if allowed portnums are configured, ignore portnums that are not in the list
|
// if allowed portnums are configured, ignore portnums that are not in the list
|
||||||
if(allowedPortnums != null && !allowedPortnums.includes(portnum)){
|
if(allowedPortnums != null && !allowedPortnums.includes(portnum)){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(portnum === Portnums.PortNum.TEXT_MESSAGE_APP) {
|
let callback = null;
|
||||||
|
let schema = null;
|
||||||
if(!collectTextMessages){
|
switch(portnum) {
|
||||||
|
case Portnums.PortNum.TEXT_MESSAGE_APP:
|
||||||
|
callback = onTextMessage;
|
||||||
|
break;
|
||||||
|
case Portnums.PortNum.POSITION_APP:
|
||||||
|
callback = onPosition;
|
||||||
|
schema = Mesh.PositionSchema;
|
||||||
|
break;
|
||||||
|
case Portnums.PortNum.NODEINFO_APP:
|
||||||
|
callback = onNodeInfo;
|
||||||
|
schema = Mesh.UserSchema;
|
||||||
|
break;
|
||||||
|
case Portnums.PortNum.WAYPOINT_APP:
|
||||||
|
callback = onWaypoint;
|
||||||
|
schema = Mesh.WaypointSchema;
|
||||||
|
break;
|
||||||
|
case Portnums.PortNum.NEIGHBORINFO_APP:
|
||||||
|
callback = onNeighborInfo;
|
||||||
|
schema = Mesh.NeighborInfoSchema;
|
||||||
|
break;
|
||||||
|
case Portnums.PortNum.TELEMETRY_APP:
|
||||||
|
callback = onTelemetry;
|
||||||
|
schema = Telemetry.TelemetrySchema;
|
||||||
|
break;
|
||||||
|
case Portnums.PortNum.TRACEROUTE_APP:
|
||||||
|
callback = onRouteDiscovery;
|
||||||
|
schema = Mesh.RouteDiscoverySchema;
|
||||||
|
break;
|
||||||
|
case Portnums.PortNum.MAP_REPORT_APP:
|
||||||
|
callback = onMapReport;
|
||||||
|
schema = Mqtt.MapReportSchema;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// handle unknown port nums here
|
||||||
|
if (logUnknownPortnums) {
|
||||||
|
const ignoredPortnums = [
|
||||||
|
Portnums.PortNum.UNKNOWN_APP,
|
||||||
|
Portnums.PortNum.TEXT_MESSAGE_COMPRESSED_APP,
|
||||||
|
Portnums.PortNum.ROUTING_APP,
|
||||||
|
Portnums.PortNum.PAXCOUNTER_APP,
|
||||||
|
Portnums.PortNum.STORE_FORWARD_APP,
|
||||||
|
Portnums.PortNum.RANGE_TEST_APP,
|
||||||
|
Portnums.PortNum.ATAK_PLUGIN,
|
||||||
|
Portnums.PortNum.ATAK_FORWARDER,
|
||||||
|
];
|
||||||
|
// ignore packets we don't want to see for now
|
||||||
|
if (portnum === undefined || ignoredPortnums.includes(portnum) || portnum > 511) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log(portnum, envelope);
|
||||||
// check if we want to ignore direct messages
|
}
|
||||||
if(ignoreDirectMessages && envelope.packet.to !== 0xFFFFFFFF){
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (callback !== null) {
|
||||||
|
if (schema !== null) {
|
||||||
|
try {
|
||||||
|
payload = fromBinary(schema, payload);
|
||||||
|
} catch(e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await callback(envelope, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onTextMessage(envelope, payload) {
|
||||||
if(logKnownPacketTypes) {
|
if(logKnownPacketTypes) {
|
||||||
console.log("TEXT_MESSAGE_APP", {
|
console.log('TEXT_MESSAGE_APP', {
|
||||||
to: envelope.packet.to.toString(16),
|
to: envelope.packet.to.toString(16),
|
||||||
from: envelope.packet.from.toString(16),
|
from: envelope.packet.from.toString(16),
|
||||||
text: payload.toString(),
|
text: payload.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!collectorEnabled.textMessages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we want to ignore direct messages
|
||||||
|
if(!collectorEnabled.directMessages && envelope.packet.to !== 0xFFFFFFFF){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.textMessage.create({
|
await prisma.textMessage.create({
|
||||||
data: {
|
data: {
|
||||||
@ -803,32 +888,30 @@ client.on("message", async (topic, message) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(portnum === Portnums.PortNum.POSITION_APP) {
|
async function onPosition(envelope, payload) {
|
||||||
const position = fromBinary(Mesh.PositionSchema, payload);
|
|
||||||
|
|
||||||
if(logKnownPacketTypes){
|
if(logKnownPacketTypes){
|
||||||
console.log("POSITION_APP", {
|
console.log('POSITION_APP', {
|
||||||
from: envelope.packet.from.toString(16),
|
from: envelope.packet.from.toString(16),
|
||||||
position: position,
|
position: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// process position
|
// process position
|
||||||
if(position.latitudeI != null && position.longitudeI){
|
if(payload.latitudeI != null && payload.longitudeI){
|
||||||
|
|
||||||
|
const bitfield = envelope.packet.payloadVariant.value.bitfield;
|
||||||
// if bitfield is not available, we are on firmware v2.4 or below
|
// if bitfield is not available, we are on firmware v2.4 or below
|
||||||
// if configured, position packets should have their precision reduced
|
// if configured, position packets should have their precision reduced
|
||||||
if (bitfield == null && oldFirmwarePositionPrecision != null) {
|
if (bitfield == null && oldFirmwarePositionPrecision != null) {
|
||||||
|
|
||||||
// adjust precision of latitude and longitude
|
// adjust precision of latitude and longitude
|
||||||
position.latitudeI = PositionUtil.setPositionPrecision(position.latitudeI, oldFirmwarePositionPrecision);
|
payload.latitudeI = PositionUtil.setPositionPrecision(payload.latitudeI, oldFirmwarePositionPrecision);
|
||||||
position.longitudeI = PositionUtil.setPositionPrecision(position.longitudeI, oldFirmwarePositionPrecision);
|
payload.longitudeI = PositionUtil.setPositionPrecision(payload.longitudeI, oldFirmwarePositionPrecision);
|
||||||
|
|
||||||
// update position precision on packet to show that it is no longer full precision
|
// update position precision on packet to show that it is no longer full precision
|
||||||
position.precisionBits = oldFirmwarePositionPrecision;
|
payload.precisionBits = oldFirmwarePositionPrecision;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -840,10 +923,10 @@ client.on("message", async (topic, message) => {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
position_updated_at: new Date(),
|
position_updated_at: new Date(),
|
||||||
latitude: position.latitudeI,
|
latitude: payload.latitudeI,
|
||||||
longitude: position.longitudeI,
|
longitude: payload.longitudeI,
|
||||||
altitude: position.altitude !== 0 ? position.altitude : null,
|
altitude: payload.altitude !== 0 ? payload.altitude : null,
|
||||||
position_precision: position.precisionBits,
|
position_precision: payload.precisionBits,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -853,7 +936,7 @@ client.on("message", async (topic, message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// don't collect position history if not enabled, but we still want to update the node above
|
// don't collect position history if not enabled, but we still want to update the node above
|
||||||
if(!collectPositions){
|
if (!collectorEnabled.positions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -881,9 +964,9 @@ client.on("message", async (topic, message) => {
|
|||||||
packet_id: envelope.packet.id,
|
packet_id: envelope.packet.id,
|
||||||
channel_id: envelope.channelId,
|
channel_id: envelope.channelId,
|
||||||
gateway_id: envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null,
|
gateway_id: envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null,
|
||||||
latitude: position.latitudeI,
|
latitude: payload.latitudeI,
|
||||||
longitude: position.longitudeI,
|
longitude: payload.longitudeI,
|
||||||
altitude: position.altitude,
|
altitude: payload.altitude,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -891,17 +974,13 @@ client.on("message", async (topic, message) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(portnum === Portnums.PortNum.NODEINFO_APP) {
|
async function onNodeInfo(envelope, payload) {
|
||||||
|
|
||||||
const user = fromBinary(Mesh.UserSchema, payload);
|
|
||||||
|
|
||||||
if(logKnownPacketTypes) {
|
if(logKnownPacketTypes) {
|
||||||
console.log("NODEINFO_APP", {
|
console.log('NODEINFO_APP', {
|
||||||
from: envelope.packet.from.toString(16),
|
from: envelope.packet.from.toString(16),
|
||||||
user: user,
|
user: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -913,55 +992,51 @@ client.on("message", async (topic, message) => {
|
|||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
node_id: envelope.packet.from,
|
node_id: envelope.packet.from,
|
||||||
long_name: user.longName,
|
long_name: payload.longName,
|
||||||
short_name: user.shortName,
|
short_name: payload.shortName,
|
||||||
hardware_model: user.hwModel,
|
hardware_model: payload.hwModel,
|
||||||
is_licensed: user.isLicensed === true,
|
is_licensed: payload.isLicensed === true,
|
||||||
role: user.role,
|
role: payload.role,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
long_name: user.longName,
|
long_name: payload.longName,
|
||||||
short_name: user.shortName,
|
short_name: payload.shortName,
|
||||||
hardware_model: user.hwModel,
|
hardware_model: payload.hwModel,
|
||||||
is_licensed: user.isLicensed === true,
|
is_licensed: payload.isLicensed === true,
|
||||||
role: user.role,
|
role: payload.role,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(portnum === Portnums.PortNum.WAYPOINT_APP) {
|
async function onWaypoint(envelope, payload) {
|
||||||
|
|
||||||
if(!collectWaypoints){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const waypoint = fromBinary(Mesh.WaypointSchema, payload);
|
|
||||||
|
|
||||||
if(logKnownPacketTypes) {
|
if(logKnownPacketTypes) {
|
||||||
console.log("WAYPOINT_APP", {
|
console.log('WAYPOINT_APP', {
|
||||||
to: envelope.packet.to.toString(16),
|
to: envelope.packet.to.toString(16),
|
||||||
from: envelope.packet.from.toString(16),
|
from: envelope.packet.from.toString(16),
|
||||||
waypoint: waypoint,
|
waypoint: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!collectorEnabled.waypoints) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.waypoint.create({
|
await prisma.waypoint.create({
|
||||||
data: {
|
data: {
|
||||||
to: envelope.packet.to,
|
to: envelope.packet.to,
|
||||||
from: envelope.packet.from,
|
from: envelope.packet.from,
|
||||||
waypoint_id: waypoint.id,
|
waypoint_id: payload.id,
|
||||||
latitude: waypoint.latitudeI,
|
latitude: payload.latitudeI,
|
||||||
longitude: waypoint.longitudeI,
|
longitude: payload.longitudeI,
|
||||||
expire: waypoint.expire,
|
expire: payload.expire,
|
||||||
locked_to: waypoint.lockedTo,
|
locked_to: payload.lockedTo,
|
||||||
name: waypoint.name,
|
name: payload.name,
|
||||||
description: waypoint.description,
|
description: payload.description,
|
||||||
icon: waypoint.icon,
|
icon: payload.icon,
|
||||||
channel: envelope.packet.channel,
|
channel: envelope.packet.channel,
|
||||||
packet_id: envelope.packet.id,
|
packet_id: envelope.packet.id,
|
||||||
channel_id: envelope.channelId,
|
channel_id: envelope.channelId,
|
||||||
@ -971,17 +1046,103 @@ client.on("message", async (topic, message) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(portnum === Portnums.PortNum.NEIGHBORINFO_APP) {
|
async function onMapReport(envelope, payload) {
|
||||||
|
|
||||||
const neighbourInfo = fromBinary(Mesh.NeighborInfoSchema, payload);
|
|
||||||
|
|
||||||
if(logKnownPacketTypes) {
|
if(logKnownPacketTypes) {
|
||||||
console.log("NEIGHBORINFO_APP", {
|
console.log('MAP_REPORT_APP', {
|
||||||
from: envelope.packet.from.toString(16),
|
from: envelope.packet.from.toString(16),
|
||||||
neighbour_info: neighbourInfo,
|
map_report: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// create or update node in db
|
||||||
|
try {
|
||||||
|
|
||||||
|
// data to set on node
|
||||||
|
const data = {
|
||||||
|
long_name: payload.longName,
|
||||||
|
short_name: payload.shortName,
|
||||||
|
hardware_model: payload.hwModel,
|
||||||
|
role: payload.role,
|
||||||
|
latitude: payload.latitudeI,
|
||||||
|
longitude: payload.longitudeI,
|
||||||
|
altitude: payload.altitude !== 0 ? payload.altitude : null,
|
||||||
|
firmware_version: payload.firmwareVersion,
|
||||||
|
region: payload.region,
|
||||||
|
modem_preset: payload.modemPreset,
|
||||||
|
has_default_channel: payload.hasDefaultChannel,
|
||||||
|
position_precision: payload.positionPrecision,
|
||||||
|
num_online_local_nodes: payload.numOnlineLocalNodes,
|
||||||
|
position_updated_at: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await prisma.node.upsert({
|
||||||
|
where: {
|
||||||
|
node_id: envelope.packet.from,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
node_id: envelope.packet.from,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
update: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't collect map report history if not enabled, but we still want to update the node above
|
||||||
|
if (!collectorEnabled.mapReports) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// find an existing map with duplicate information created in the last 60 seconds
|
||||||
|
const existingDuplicateMapReport = await prisma.mapReport.findFirst({
|
||||||
|
where: {
|
||||||
|
node_id: envelope.packet.from,
|
||||||
|
long_name: payload.longName,
|
||||||
|
short_name: payload.shortName,
|
||||||
|
created_at: {
|
||||||
|
gte: new Date(Date.now() - 60000), // created in the last 60 seconds
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// create map report if no duplicates found
|
||||||
|
if(!existingDuplicateMapReport){
|
||||||
|
await prisma.mapReport.create({
|
||||||
|
data: {
|
||||||
|
node_id: envelope.packet.from,
|
||||||
|
long_name: payload.longName,
|
||||||
|
short_name: payload.shortName,
|
||||||
|
role: payload.role,
|
||||||
|
hardware_model: payload.hwModel,
|
||||||
|
firmware_version: payload.firmwareVersion,
|
||||||
|
region: payload.region,
|
||||||
|
modem_preset: payload.modemPreset,
|
||||||
|
has_default_channel: payload.hasDefaultChannel,
|
||||||
|
latitude: payload.latitudeI,
|
||||||
|
longitude: payload.longitudeI,
|
||||||
|
altitude: payload.altitude,
|
||||||
|
position_precision: payload.positionPrecision,
|
||||||
|
num_online_local_nodes: payload.numOnlineLocalNodes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onNeighborInfo(envelope, payload) {
|
||||||
|
if(logKnownPacketTypes) {
|
||||||
|
console.log('NEIGHBORINFO_APP', {
|
||||||
|
from: envelope.packet.from.toString(16),
|
||||||
|
neighbor_info: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -993,8 +1154,8 @@ client.on("message", async (topic, message) => {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
neighbours_updated_at: new Date(),
|
neighbours_updated_at: new Date(),
|
||||||
neighbour_broadcast_interval_secs: neighbourInfo.nodeBroadcastIntervalSecs,
|
neighbour_broadcast_interval_secs: payload.nodeBroadcastIntervalSecs,
|
||||||
neighbours: neighbourInfo.neighbors.map((neighbour) => {
|
neighbours: payload.neighbors.map((neighbour) => {
|
||||||
return {
|
return {
|
||||||
node_id: neighbour.nodeId,
|
node_id: neighbour.nodeId,
|
||||||
snr: neighbour.snr,
|
snr: neighbour.snr,
|
||||||
@ -1007,7 +1168,7 @@ client.on("message", async (topic, message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// don't store all neighbour infos, but we want to update the existing node above
|
// don't store all neighbour infos, but we want to update the existing node above
|
||||||
if(!collectNeighbourInfo){
|
if (!collectorEnabled.neighborInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1016,8 +1177,8 @@ client.on("message", async (topic, message) => {
|
|||||||
await prisma.neighbourInfo.create({
|
await prisma.neighbourInfo.create({
|
||||||
data: {
|
data: {
|
||||||
node_id: envelope.packet.from,
|
node_id: envelope.packet.from,
|
||||||
node_broadcast_interval_secs: neighbourInfo.nodeBroadcastIntervalSecs,
|
node_broadcast_interval_secs: payload.nodeBroadcastIntervalSecs,
|
||||||
neighbours: neighbourInfo.neighbors.map((neighbour) => {
|
neighbours: payload.neighbors.map((neighbour) => {
|
||||||
return {
|
return {
|
||||||
node_id: neighbour.nodeId,
|
node_id: neighbour.nodeId,
|
||||||
snr: neighbour.snr,
|
snr: neighbour.snr,
|
||||||
@ -1028,31 +1189,62 @@ client.on("message", async (topic, message) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(portnum === Portnums.PortNum.TELEMETRY_APP) {
|
async function onRouteDiscovery(envelope, payload) {
|
||||||
|
|
||||||
const telemetry = fromBinary(Telemetry.TelemetrySchema, payload);
|
|
||||||
|
|
||||||
if(logKnownPacketTypes) {
|
if(logKnownPacketTypes) {
|
||||||
console.log("TELEMETRY_APP", {
|
console.log('TRACEROUTE_APP', {
|
||||||
|
to: envelope.packet.to.toString(16),
|
||||||
from: envelope.packet.from.toString(16),
|
from: envelope.packet.from.toString(16),
|
||||||
telemetry: telemetry,
|
want_response: envelope.packet.payloadVariant.value.wantResponse,
|
||||||
|
route_discovery: payload,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.traceRoute.create({
|
||||||
|
data: {
|
||||||
|
to: envelope.packet.to,
|
||||||
|
from: envelope.packet.from,
|
||||||
|
want_response: envelope.packet.payloadVariant.value.wantResponse,
|
||||||
|
route: payload.route,
|
||||||
|
snr_towards: payload.snrTowards,
|
||||||
|
route_back: payload.routeBack,
|
||||||
|
snr_back: payload.snrBack,
|
||||||
|
channel: envelope.packet.channel,
|
||||||
|
packet_id: envelope.packet.id,
|
||||||
|
channel_id: envelope.channelId,
|
||||||
|
gateway_id: envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onTelemetry(envelope, payload) {
|
||||||
|
// we need to do some work on the packet to log it properly
|
||||||
|
const telemetryType = payload.variant.case
|
||||||
|
if(logKnownPacketTypes) {
|
||||||
|
console.log('TELEMETRY_APP', {
|
||||||
|
from: envelope.packet.from.toString(16),
|
||||||
|
type: telemetryType,
|
||||||
|
telemetry: payload.variant.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload = payload.variant.value
|
||||||
|
|
||||||
// data to update
|
// data to update
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
// handle device metrics
|
// handle device metrics
|
||||||
if(telemetry.deviceMetrics){
|
if (telemetryType === 'deviceMetrics'){
|
||||||
|
|
||||||
data.battery_level = telemetry.deviceMetrics.batteryLevel !== 0 ? telemetry.deviceMetrics.batteryLevel : null;
|
data.battery_level = payload.batteryLevel !== 0 ? payload.batteryLevel : null;
|
||||||
data.voltage = telemetry.deviceMetrics.voltage !== 0 ? telemetry.deviceMetrics.voltage : null;
|
data.voltage = payload.voltage !== 0 ? payload.voltage : null;
|
||||||
data.channel_utilization = telemetry.deviceMetrics.channelUtilization !== 0 ? telemetry.deviceMetrics.channelUtilization : null;
|
data.channel_utilization = payload.channelUtilization !== 0 ? payload.channelUtilization : null;
|
||||||
data.air_util_tx = telemetry.deviceMetrics.airUtilTx !== 0 ? telemetry.deviceMetrics.airUtilTx : null;
|
data.air_util_tx = payload.airUtilTx !== 0 ? payload.airUtilTx : null;
|
||||||
data.uptime_seconds = telemetry.deviceMetrics.uptimeSeconds !== 0 ? telemetry.deviceMetrics.uptimeSeconds : null;
|
data.uptime_seconds = payload.uptimeSeconds !== 0 ? payload.uptimeSeconds : null;
|
||||||
|
|
||||||
// create device metric
|
// create device metric
|
||||||
try {
|
try {
|
||||||
@ -1091,20 +1283,20 @@ client.on("message", async (topic, message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle environment metrics
|
// handle environment metrics
|
||||||
if(telemetry.environmentMetrics){
|
if(telemetryType === 'environmentMetrics'){
|
||||||
|
|
||||||
// get metric values
|
// get metric values
|
||||||
const temperature = telemetry.environmentMetrics.temperature !== 0 ? telemetry.environmentMetrics.temperature : null;
|
const temperature = payload.temperature !== 0 ? payload.temperature : null;
|
||||||
const relativeHumidity = telemetry.environmentMetrics.relativeHumidity !== 0 ? telemetry.environmentMetrics.relativeHumidity : null;
|
const relativeHumidity = payload.relativeHumidity !== 0 ? payload.relativeHumidity : null;
|
||||||
const barometricPressure = telemetry.environmentMetrics.barometricPressure !== 0 ? telemetry.environmentMetrics.barometricPressure : null;
|
const barometricPressure = payload.barometricPressure !== 0 ? payload.barometricPressure : null;
|
||||||
const gasResistance = telemetry.environmentMetrics.gasResistance !== 0 ? telemetry.environmentMetrics.gasResistance : null;
|
const gasResistance = payload.gasResistance !== 0 ? payload.gasResistance : null;
|
||||||
const voltage = telemetry.environmentMetrics.voltage !== 0 ? telemetry.environmentMetrics.voltage : null;
|
const voltage = payload.voltage !== 0 ? payload.voltage : null;
|
||||||
const current = telemetry.environmentMetrics.current !== 0 ? telemetry.environmentMetrics.current : null;
|
const current = payload.current !== 0 ? payload.current : null;
|
||||||
const iaq = telemetry.environmentMetrics.iaq !== 0 ? telemetry.environmentMetrics.iaq : null;
|
const iaq = payload.iaq !== 0 ? payload.iaq : null;
|
||||||
const windDirection = telemetry.environmentMetrics.windDirection;
|
const windDirection = payload.windDirection;
|
||||||
const windSpeed = telemetry.environmentMetrics.windSpeed;
|
const windSpeed = payload.windSpeed;
|
||||||
const windGust = telemetry.environmentMetrics.windGust;
|
const windGust = payload.windGust;
|
||||||
const windLull = telemetry.environmentMetrics.windLull;
|
const windLull = payload.windLull;
|
||||||
|
|
||||||
// set metrics to update on node table
|
// set metrics to update on node table
|
||||||
data.temperature = temperature;
|
data.temperature = temperature;
|
||||||
@ -1153,15 +1345,15 @@ client.on("message", async (topic, message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle power metrics
|
// handle power metrics
|
||||||
if(telemetry.powerMetrics){
|
if(telemetryType === 'powerMetrics'){
|
||||||
|
|
||||||
// get metric values
|
// get metric values
|
||||||
const ch1Voltage = telemetry.powerMetrics.ch1Voltage !== 0 ? telemetry.powerMetrics.ch1Voltage : null;
|
const ch1Voltage = payload.ch1Voltage !== 0 ? payload.ch1Voltage : null;
|
||||||
const ch1Current = telemetry.powerMetrics.ch1Current !== 0 ? telemetry.powerMetrics.ch1Current : null;
|
const ch1Current = payload.ch1Current !== 0 ? payload.ch1Current : null;
|
||||||
const ch2Voltage = telemetry.powerMetrics.ch2Voltage !== 0 ? telemetry.powerMetrics.ch2Voltage : null;
|
const ch2Voltage = payload.ch2Voltage !== 0 ? payload.ch2Voltage : null;
|
||||||
const ch2Current = telemetry.powerMetrics.ch2Current !== 0 ? telemetry.powerMetrics.ch2Current : null;
|
const ch2Current = payload.ch2Current !== 0 ? payload.ch2Current : null;
|
||||||
const ch3Voltage = telemetry.powerMetrics.ch3Voltage !== 0 ? telemetry.powerMetrics.ch3Voltage : null;
|
const ch3Voltage = payload.ch3Voltage !== 0 ? payload.ch3Voltage : null;
|
||||||
const ch3Current = telemetry.powerMetrics.ch3Current !== 0 ? telemetry.powerMetrics.ch3Current : null;
|
const ch3Current = payload.ch3Current !== 0 ? payload.ch3Current : null;
|
||||||
|
|
||||||
// create power metric
|
// create power metric
|
||||||
try {
|
try {
|
||||||
@ -1212,160 +1404,4 @@ client.on("message", async (topic, message) => {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(portnum === Portnums.PortNum.TRACEROUTE_APP) {
|
|
||||||
|
|
||||||
const routeDiscovery = fromBinary(Mesh.RouteDiscoverySchema, payload);
|
|
||||||
|
|
||||||
if(logKnownPacketTypes) {
|
|
||||||
console.log("TRACEROUTE_APP", {
|
|
||||||
to: envelope.packet.to.toString(16),
|
|
||||||
from: envelope.packet.from.toString(16),
|
|
||||||
want_response: payload.wantResponse,
|
|
||||||
route_discovery: routeDiscovery,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await prisma.traceRoute.create({
|
|
||||||
data: {
|
|
||||||
to: envelope.packet.to,
|
|
||||||
from: envelope.packet.from,
|
|
||||||
want_response: envelope.packet.decoded.wantResponse,
|
|
||||||
route: routeDiscovery.route,
|
|
||||||
snr_towards: routeDiscovery.snrTowards,
|
|
||||||
route_back: routeDiscovery.routeBack,
|
|
||||||
snr_back: routeDiscovery.snrBack,
|
|
||||||
channel: envelope.packet.channel,
|
|
||||||
packet_id: envelope.packet.id,
|
|
||||||
channel_id: envelope.channelId,
|
|
||||||
gateway_id: envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(portnum === Portnums.PortNum.MAP_REPORT_APP) {
|
|
||||||
|
|
||||||
const mapReport = fromBinary(Mqtt.MapReportSchema, payload);
|
|
||||||
|
|
||||||
if(logKnownPacketTypes) {
|
|
||||||
console.log("MAP_REPORT_APP", {
|
|
||||||
from: envelope.packet.from.toString(16),
|
|
||||||
map_report: mapReport,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// create or update node in db
|
|
||||||
try {
|
|
||||||
|
|
||||||
// data to set on node
|
|
||||||
const data = {
|
|
||||||
long_name: mapReport.longName,
|
|
||||||
short_name: mapReport.shortName,
|
|
||||||
hardware_model: mapReport.hwModel,
|
|
||||||
role: mapReport.role,
|
|
||||||
latitude: mapReport.latitudeI,
|
|
||||||
longitude: mapReport.longitudeI,
|
|
||||||
altitude: mapReport.altitude !== 0 ? mapReport.altitude : null,
|
|
||||||
firmware_version: mapReport.firmwareVersion,
|
|
||||||
region: mapReport.region,
|
|
||||||
modem_preset: mapReport.modemPreset,
|
|
||||||
has_default_channel: mapReport.hasDefaultChannel,
|
|
||||||
position_precision: mapReport.positionPrecision,
|
|
||||||
num_online_local_nodes: mapReport.numOnlineLocalNodes,
|
|
||||||
position_updated_at: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await prisma.node.upsert({
|
|
||||||
where: {
|
|
||||||
node_id: envelope.packet.from,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
node_id: envelope.packet.from,
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
update: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't collect map report history if not enabled, but we still want to update the node above
|
|
||||||
if(!collectMapReports){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// find an existing map with duplicate information created in the last 60 seconds
|
|
||||||
const existingDuplicateMapReport = await prisma.mapReport.findFirst({
|
|
||||||
where: {
|
|
||||||
node_id: envelope.packet.from,
|
|
||||||
long_name: mapReport.longName,
|
|
||||||
short_name: mapReport.shortName,
|
|
||||||
created_at: {
|
|
||||||
gte: new Date(Date.now() - 60000), // created in the last 60 seconds
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// create map report if no duplicates found
|
|
||||||
if(!existingDuplicateMapReport){
|
|
||||||
await prisma.mapReport.create({
|
|
||||||
data: {
|
|
||||||
node_id: envelope.packet.from,
|
|
||||||
long_name: mapReport.longName,
|
|
||||||
short_name: mapReport.shortName,
|
|
||||||
role: mapReport.role,
|
|
||||||
hardware_model: mapReport.hwModel,
|
|
||||||
firmware_version: mapReport.firmwareVersion,
|
|
||||||
region: mapReport.region,
|
|
||||||
modem_preset: mapReport.modemPreset,
|
|
||||||
has_default_channel: mapReport.hasDefaultChannel,
|
|
||||||
latitude: mapReport.latitudeI,
|
|
||||||
longitude: mapReport.longitudeI,
|
|
||||||
altitude: mapReport.altitude,
|
|
||||||
position_precision: mapReport.positionPrecision,
|
|
||||||
num_online_local_nodes: mapReport.numOnlineLocalNodes,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
if (logUnknownPortnums) {
|
|
||||||
const ignoredPortnums = [
|
|
||||||
Portnums.PortNum.UNKNOWN_APP,
|
|
||||||
Portnums.PortNum.TEXT_MESSAGE_COMPRESSED_APP,
|
|
||||||
Portnums.PortNum.ROUTING_APP,
|
|
||||||
Portnums.PortNum.PAXCOUNTER_APP,
|
|
||||||
Portnums.PortNum.STORE_FORWARD_APP,
|
|
||||||
Portnums.PortNum.RANGE_TEST_APP,
|
|
||||||
Portnums.PortNum.ATAK_PLUGIN,
|
|
||||||
Portnums.PortNum.ATAK_FORWARDER,
|
|
||||||
];
|
|
||||||
// ignore packets we don't want to see for now
|
|
||||||
if (portnum === undefined || ignoredPortnums.includes(portnum) || portnum > 511) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(portnum, envelope);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// ignore errors
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^2.2.5",
|
"@bufbuild/protobuf": "^2.2.5",
|
||||||
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
|
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
class NodeIdUtil {
|
export default class NodeIdUtil {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the provided hex id to a numeric id, for example: !FFFFFFFF to 4294967295
|
* Converts the provided hex id to a numeric id, for example: !FFFFFFFF to 4294967295
|
||||||
@ -19,5 +19,3 @@ class NodeIdUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = NodeIdUtil;
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
class PositionUtil {
|
export default class PositionUtil {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obfuscates the provided latitude or longitude down to the provided precision in bits.
|
* Obfuscates the provided latitude or longitude down to the provided precision in bits.
|
||||||
@ -62,5 +62,3 @@ class PositionUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PositionUtil;
|
|
||||||
|
21
webapp/frontend/package-lock.json
generated
21
webapp/frontend/package-lock.json
generated
@ -15,6 +15,8 @@
|
|||||||
"chartjs-adapter-moment": "^1.0.1",
|
"chartjs-adapter-moment": "^1.0.1",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet-arrowheads": "^1.4.0",
|
||||||
|
"leaflet-geometryutil": "^0.10.3",
|
||||||
"leaflet-groupedlayercontrol": "^0.6.1",
|
"leaflet-groupedlayercontrol": "^0.6.1",
|
||||||
"leaflet.markercluster": "^1.5.3",
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
@ -2700,6 +2702,25 @@
|
|||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet-arrowheads": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet-arrowheads/-/leaflet-arrowheads-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-aIjsmoWe1VJXaGOpKpS6E8EzN2vpx3GGCNP/FxQteLVzAg5xMID7elf9hj/1CWLJo8FuGRjSvKkUQDj7mocrYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"leaflet": "^1.7.1",
|
||||||
|
"leaflet-geometryutil": "^0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/leaflet-geometryutil": {
|
||||||
|
"version": "0.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet-geometryutil/-/leaflet-geometryutil-0.10.3.tgz",
|
||||||
|
"integrity": "sha512-Qeas+KsnenE0Km/ydt8km3AqFe7kJhVwuLdbCYM2xe2epsxv5UFEaVJiagvP9fnxS8QvBNbm7DJlDA0tkKo9VA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"leaflet": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/leaflet-groupedlayercontrol": {
|
"node_modules/leaflet-groupedlayercontrol": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet-groupedlayercontrol/-/leaflet-groupedlayercontrol-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet-groupedlayercontrol/-/leaflet-groupedlayercontrol-0.6.1.tgz",
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
"chartjs-adapter-moment": "^1.0.1",
|
"chartjs-adapter-moment": "^1.0.1",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet-arrowheads": "^1.4.0",
|
||||||
|
"leaflet-geometryutil": "^0.10.3",
|
||||||
"leaflet-groupedlayercontrol": "^0.6.1",
|
"leaflet-groupedlayercontrol": "^0.6.1",
|
||||||
"leaflet.markercluster": "^1.5.3",
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
386
webapp/frontend/public/text-message-embed.html
Normal file
386
webapp/frontend/public/text-message-embed.html
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<title>Meshtastic Messages</title>
|
||||||
|
|
||||||
|
<!-- tailwind css -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- moment -->
|
||||||
|
<script src="https://app.unpkg.com/moment@2.29.1/files/dist/moment.js"></script>
|
||||||
|
|
||||||
|
<!-- vuejs -->
|
||||||
|
<script src="https://app.unpkg.com/vue@3.4.26/files/dist/vue.global.js"></script>
|
||||||
|
|
||||||
|
<!-- axios -->
|
||||||
|
<script src="https://app.unpkg.com/axios@1.6.8/files/dist/axios.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
/* used to prevent ui flicker before vuejs loads */
|
||||||
|
[v-cloak] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="h-full">
|
||||||
|
<div id="app" v-cloak>
|
||||||
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
|
|
||||||
|
<!-- empty state -->
|
||||||
|
<div v-if="messages.length === 0" class="flex h-full">
|
||||||
|
<div class="flex flex-col mx-auto my-auto p-4 text-gray-500 text-center">
|
||||||
|
<div class="mb-2 mx-auto">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-10">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold">No Messages</div>
|
||||||
|
<div>There's no messages yet...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- note: must use flex-col-reverse to prevent ui scrolling when adding older messages to ui -->
|
||||||
|
<div v-show="messages.length > 0" id="messages" class="h-full flex flex-col-reverse p-3 overflow-y-auto">
|
||||||
|
|
||||||
|
<!-- messages -->
|
||||||
|
<div :key="message.id" v-for="message of reversedMessages" class="max-w-xl items-start my-1.5">
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
|
||||||
|
<div class="mr-2 mt-2">
|
||||||
|
<a target="_blank" :href="`/?node_id=${message.from}`">
|
||||||
|
<div class="flex rounded-full h-12 w-12 text-white shadow" :class="[ `bg-[${getNodeColour(message.from)}]`, `text-[${getNodeTextColour(message.from)}]` ]">
|
||||||
|
<div class="mx-auto my-auto drop-shadow-sm">{{ getNodeShortName(message.from) }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
|
||||||
|
<!-- sender -->
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
<a target="_blank" :href="`/?node_id=${message.from}`" class="hover:text-blue-500">
|
||||||
|
<span>{{ getNodeLongName(message.from) }}</span>
|
||||||
|
</a>
|
||||||
|
<span v-if="message.to.toString() !== '4294967295'">
|
||||||
|
<span> → </span>
|
||||||
|
<a target="_blank" :href="`/?node_id=${message.to}`" class="hover:text-blue-500">{{ getNodeName(message.to) }}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- message -->
|
||||||
|
<div @click="message.is_details_expanded = !message.is_details_expanded" class="flex">
|
||||||
|
<div class="border border-gray-300 rounded-xl shadow overflow-hidden bg-[#efefef] divide-y">
|
||||||
|
<div class="w-full space-y-0.5 px-2.5 py-1" v-html="escapeMessageText(message.text)" style="white-space:pre-wrap;word-break:break-word;"></div>
|
||||||
|
<div v-if="message.is_details_expanded" class="text-xs text-gray-500 px-2 py-1">
|
||||||
|
<span :title="message.created_at">{{ formatMessageTimestamp(message.created_at) }}</span>
|
||||||
|
<span> • Gated by <a target="_blank" :href="`/?node_id=${message.gateway_id}`" class="hover:text-blue-500">{{ getNodeName(message.gateway_id) }}</a></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- load previous -->
|
||||||
|
<button v-show="!isLoadingPrevious && hasMorePrevious" id="load-previous" @click="loadPrevious" type="button" class="flex space-x-2 mx-auto bg-gray-200 px-3 py-1 hover:bg-gray-300 rounded-full shadow">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||||
|
</svg>
|
||||||
|
<span>Load Previous</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
to: null,
|
||||||
|
from: null,
|
||||||
|
channelId: null,
|
||||||
|
gatewayId: null,
|
||||||
|
|
||||||
|
isLoadingPrevious: false,
|
||||||
|
isLoadingMore: false,
|
||||||
|
shouldAutoScroll: true,
|
||||||
|
loadPreviousObserver: null,
|
||||||
|
hasMorePrevious: true,
|
||||||
|
|
||||||
|
messages: [],
|
||||||
|
nodesById: {},
|
||||||
|
|
||||||
|
moment: window.moment,
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted: function() {
|
||||||
|
|
||||||
|
// parse url params
|
||||||
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
this.to = queryParams.get('to');
|
||||||
|
this.from = queryParams.get('from');
|
||||||
|
this.channelId = queryParams.get('channel_id');
|
||||||
|
this.gatewayId = queryParams.get('gateway_id');
|
||||||
|
this.directMessageNodeIds = queryParams.get('direct_message_node_ids');
|
||||||
|
this.count = queryParams.get('count');
|
||||||
|
|
||||||
|
// listen for scrolling of messages list
|
||||||
|
document.getElementById("messages").addEventListener("scroll", (event) => {
|
||||||
|
|
||||||
|
// check if messages is scrolled to bottom
|
||||||
|
const element = event.target;
|
||||||
|
const isAtBottom = element.scrollTop === (element.scrollHeight - element.offsetHeight);
|
||||||
|
|
||||||
|
// we want to auto scroll if user is at bottom of messages list
|
||||||
|
this.shouldAutoScroll = isAtBottom;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup intersection observer
|
||||||
|
this.loadPreviousObserver = new IntersectionObserver((entries) => {
|
||||||
|
const loadMoreElement = entries[0];
|
||||||
|
if(loadMoreElement && loadMoreElement.isIntersecting){
|
||||||
|
this.loadPrevious();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initialLoad();
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async initialLoad() {
|
||||||
|
|
||||||
|
// load 1 page of previous messages
|
||||||
|
await this.loadPrevious();
|
||||||
|
|
||||||
|
// scroll to bottom
|
||||||
|
this.scrollToBottom();
|
||||||
|
|
||||||
|
// setup auto loading previous
|
||||||
|
this.loadPreviousObserver.observe(document.querySelector("#load-previous"));
|
||||||
|
|
||||||
|
// load more every few seconds
|
||||||
|
setInterval(async () => {
|
||||||
|
await this.loadMore();
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
},
|
||||||
|
async loadPrevious() {
|
||||||
|
|
||||||
|
// do nothing if already loading
|
||||||
|
if(this.isLoadingPrevious){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoadingPrevious = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await window.axios.get('/api/v1/text-messages', {
|
||||||
|
params: {
|
||||||
|
to: this.to,
|
||||||
|
from: this.from,
|
||||||
|
channel_id: this.channelId,
|
||||||
|
gateway_id: this.gatewayId,
|
||||||
|
direct_message_node_ids: this.directMessageNodeIds,
|
||||||
|
count: this.count,
|
||||||
|
order: "desc",
|
||||||
|
last_id: this.oldestMessageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// add messages to start of existing messages
|
||||||
|
const messages = response.data.text_messages;
|
||||||
|
for(const message of messages){
|
||||||
|
this.messages.unshift(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// no more previous to load if previous list is empty
|
||||||
|
if(messages.length === 0){
|
||||||
|
this.hasMorePrevious = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch node info
|
||||||
|
for(const message of messages){
|
||||||
|
await this.fetchNodeInfo(message.to);
|
||||||
|
await this.fetchNodeInfo(message.from);
|
||||||
|
await this.fetchNodeInfo(message.gateway_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
} finally {
|
||||||
|
this.isLoadingPrevious = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async loadMore() {
|
||||||
|
|
||||||
|
// do nothing if already loading
|
||||||
|
if(this.isLoadingMore){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoadingMore = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await window.axios.get('/api/v1/text-messages', {
|
||||||
|
params: {
|
||||||
|
to: this.to,
|
||||||
|
from: this.from,
|
||||||
|
channel_id: this.channelId,
|
||||||
|
gateway_id: this.gatewayId,
|
||||||
|
direct_message_node_ids: this.directMessageNodeIds,
|
||||||
|
count: this.count,
|
||||||
|
order: "asc",
|
||||||
|
last_id: this.latestMessageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// add messages to end of existing messages
|
||||||
|
const messages = response.data.text_messages;
|
||||||
|
for(const message of messages){
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll to bottom
|
||||||
|
if(this.shouldAutoScroll){
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch node info
|
||||||
|
for(const message of messages){
|
||||||
|
await this.fetchNodeInfo(message.to);
|
||||||
|
await this.fetchNodeInfo(message.from);
|
||||||
|
await this.fetchNodeInfo(message.gateway_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
} finally {
|
||||||
|
this.isLoadingMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async fetchNodeInfo(nodeId) {
|
||||||
|
|
||||||
|
// do nothing if already fetched
|
||||||
|
if(nodeId in this.nodesById){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do nothing if broadcast address
|
||||||
|
if(nodeId.toString() === "4294967295"){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await window.axios.get(`/api/v1/nodes/${nodeId}`);
|
||||||
|
const node = response.data.node;
|
||||||
|
|
||||||
|
if(node){
|
||||||
|
this.nodesById[node.node_id] = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
scrollToBottom: function() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
var container = this.$el.querySelector("#messages");
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getNodeHexId(nodeId) {
|
||||||
|
return "!" + parseInt(nodeId).toString(16);
|
||||||
|
},
|
||||||
|
getNodeName(nodeId) {
|
||||||
|
|
||||||
|
// find node by id
|
||||||
|
const node = this.nodesById[nodeId];
|
||||||
|
if(!node){
|
||||||
|
return this.getNodeHexId(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `[${node.short_name}] ${node.long_name}`;
|
||||||
|
|
||||||
|
},
|
||||||
|
getNodeShortName(nodeId) {
|
||||||
|
return this.nodesById[nodeId]?.short_name?.substring(0, 4) ?? "?";
|
||||||
|
},
|
||||||
|
getNodeLongName(nodeId) {
|
||||||
|
return this.nodesById[nodeId]?.long_name ?? this.getNodeHexId(nodeId);
|
||||||
|
},
|
||||||
|
getNodeColour(nodeId) {
|
||||||
|
// convert node id to a hex colour
|
||||||
|
return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
|
||||||
|
},
|
||||||
|
getNodeTextColour(nodeId) {
|
||||||
|
|
||||||
|
// extract rgb components
|
||||||
|
const r = (nodeId & 0xFF0000) >> 16;
|
||||||
|
const g = (nodeId & 0x00FF00) >> 8;
|
||||||
|
const b = nodeId & 0x0000FF;
|
||||||
|
|
||||||
|
// calculate brightness
|
||||||
|
const brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255;
|
||||||
|
|
||||||
|
// determine text color based on brightness
|
||||||
|
return brightness > 0.5 ? "#000000" : "#FFFFFF";
|
||||||
|
|
||||||
|
},
|
||||||
|
escapeMessageText(text) {
|
||||||
|
return text.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('\n', '<br/>');
|
||||||
|
},
|
||||||
|
formatMessageTimestamp(createdAt) {
|
||||||
|
return moment(new Date(createdAt)).local().format("DD/MMM/YYYY hh:mm:ss A");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
reversedMessages() {
|
||||||
|
// ensure a copy of the array is returned in reverse order
|
||||||
|
return this.messages.map((message) => message).reverse();
|
||||||
|
},
|
||||||
|
oldestMessageId() {
|
||||||
|
|
||||||
|
if(this.messages.length > 0){
|
||||||
|
return this.messages[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
},
|
||||||
|
latestMessageId() {
|
||||||
|
|
||||||
|
if(this.messages.length > 0){
|
||||||
|
return this.messages[this.messages.length - 1].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).mount('#app');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(['showTraceRoute']);
|
const emit = defineEmits(['showTraceRoute']);
|
||||||
|
import moment from 'moment';
|
||||||
import { state } from '../../store.js';
|
import { state } from '../../store.js';
|
||||||
import { findNodeById } from '../../utils.js';
|
import { findNodeById } from '../../utils.js';
|
||||||
</script>
|
</script>
|
||||||
|
@ -64,7 +64,7 @@ import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from
|
|||||||
</div>
|
</div>
|
||||||
<div class="my-auto relative flex flex-none items-center justify-center">
|
<div class="my-auto relative flex flex-none items-center justify-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex rounded-full h-12 w-12 text-white shadow-sm" :class="[ `bg-[${getNodeColor(state.selectedTraceRoute.to)}]`, `text-[${getNodeTextColor(state.selectedTraceRoute.to)}]` ]">
|
<div class="flex rounded-full h-12 w-12 text-white shadow-sm" :style="{backgroundColor: getNodeColor(state.selectedTraceRoute.to)}" :class="[ `text-[${getNodeTextColor(state.selectedTraceRoute.to)}]` ]">
|
||||||
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.to)?.short_name ?? "?" }}</div>
|
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.to)?.short_name ?? "?" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -83,7 +83,7 @@ import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from
|
|||||||
</div>
|
</div>
|
||||||
<div class="my-auto relative flex flex-none items-center justify-center">
|
<div class="my-auto relative flex flex-none items-center justify-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex rounded-full h-12 w-12 text-white shadow" :class="[ `bg-[${getNodeColor(route)}]`, `text-[${getNodeTextColor(route)}]` ]">
|
<div class="flex rounded-full h-12 w-12 text-white shadow" :style="{backgroundColor: getNodeColor(route)}" :class="[ `text-[${getNodeTextColor(route)}]` ]">
|
||||||
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(route)?.short_name ?? "?" }}</div>
|
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(route)?.short_name ?? "?" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -102,7 +102,7 @@ import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from
|
|||||||
</div>
|
</div>
|
||||||
<div class="my-auto relative flex flex-none items-center justify-center">
|
<div class="my-auto relative flex flex-none items-center justify-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex rounded-full h-12 w-12 text-white shadow" :class="[ `bg-[${getNodeColor(state.selectedTraceRoute.from)}]`, `text-[${getNodeTextColor(state.selectedTraceRoute.from)}]` ]">
|
<div class="flex rounded-full h-12 w-12 text-white shadow" :style="{backgroundColor: getNodeColor(state.selectedTraceRoute.from)}" :class="[ `text-[${getNodeTextColor(state.selectedTraceRoute.from)}]` ]">
|
||||||
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.from)?.short_name ?? "?" }}</div>
|
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.from)?.short_name ?? "?" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -121,7 +121,7 @@ import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from
|
|||||||
</div>
|
</div>
|
||||||
<div class="my-auto relative flex flex-none items-center justify-center">
|
<div class="my-auto relative flex flex-none items-center justify-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex rounded-full h-12 w-12 text-white shadow" :class="[ `bg-[${getNodeColor(state.selectedTraceRoute.gateway_id)}]`, `text-[${getNodeTextColor(state.selectedTraceRoute.gateway_id)}]` ]">
|
<div class="flex rounded-full h-12 w-12 text-white shadow" :style="{backgroundColor: getNodeColor(state.selectedTraceRoute.gateway_id)}" :class="[ `text-[${getNodeTextColor(state.selectedTraceRoute.gateway_id)}]` ]">
|
||||||
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.gateway_id)?.short_name ?? "?" }}</div>
|
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.gateway_id)?.short_name ?? "?" }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import L from 'leaflet/dist/leaflet.js';
|
import 'leaflet/dist/leaflet.js';
|
||||||
|
const L = window.L;
|
||||||
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
|
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
|
||||||
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
|
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
|
||||||
import {
|
import {
|
||||||
|
@ -11,8 +11,11 @@ import Announcement from '../components/Announcement.vue';
|
|||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import L from 'leaflet/dist/leaflet.js';
|
import 'leaflet/dist/leaflet';
|
||||||
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
|
const L = window.L;
|
||||||
|
import 'leaflet-geometryutil';
|
||||||
|
import 'leaflet-arrowheads';
|
||||||
|
import 'leaflet.markercluster';
|
||||||
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
|
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
|
||||||
import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue';
|
import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue';
|
||||||
import { state } from '../store.js';
|
import { state } from '../store.js';
|
||||||
@ -298,6 +301,10 @@ function onNodesUpdated(nodes) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add node to cache
|
||||||
|
state.nodes.push(node);
|
||||||
|
|
||||||
// skip nodes without position
|
// skip nodes without position
|
||||||
if (!node.latitude || !node.longitude) {
|
if (!node.latitude || !node.longitude) {
|
||||||
continue;
|
continue;
|
||||||
@ -322,7 +329,7 @@ function onNodesUpdated(nodes) {
|
|||||||
let icon = icons.mqttDisconnected;
|
let icon = icons.mqttDisconnected;
|
||||||
|
|
||||||
// use offline icon for nodes older than configured node offline age
|
// use offline icon for nodes older than configured node offline age
|
||||||
if (nodesOfflineAge) {
|
if (nodesOfflineAge.value) {
|
||||||
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
||||||
if (lastUpdatedAgeInMillis > nodesOfflineAge.value * 1000) {
|
if (lastUpdatedAgeInMillis > nodesOfflineAge.value * 1000) {
|
||||||
icon = icons.offline;
|
icon = icons.offline;
|
||||||
@ -365,8 +372,7 @@ function onNodesUpdated(nodes) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push node and marker to cache
|
// Push node marker to cache
|
||||||
state.nodes.push(node);
|
|
||||||
state.nodeMarkers[node.node_id] = marker;
|
state.nodeMarkers[node.node_id] = marker;
|
||||||
|
|
||||||
// show node info tooltip when clicking node marker
|
// show node info tooltip when clicking node marker
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
const path = require('path');
|
import path from "path";
|
||||||
const express = require('express');
|
import { fileURLToPath } from 'url';
|
||||||
const compression = require('compression');
|
import express from "express";
|
||||||
const commandLineArgs = require("command-line-args");
|
import compression from "compression";
|
||||||
const commandLineUsage = require("command-line-usage");
|
import commandLineArgs from "command-line-args";
|
||||||
|
import commandLineUsage from "command-line-usage";
|
||||||
|
|
||||||
// protobuf imports
|
// protobuf imports
|
||||||
const { Mesh, Config } = require("@meshtastic/protobufs");
|
import { Mesh, Config } from "@meshtastic/protobufs";
|
||||||
|
|
||||||
// create prisma db client
|
// create prisma db client
|
||||||
const { PrismaClient } = require("@prisma/client");
|
import { PrismaClient } from "@prisma/client";
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// return big ints as string when using JSON.stringify
|
// return big ints as string when using JSON.stringify
|
||||||
@ -46,7 +47,7 @@ if(options.help){
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
console.log(usage);
|
console.log(usage);
|
||||||
return;
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get options and fallback to default values
|
// get options and fallback to default values
|
||||||
@ -76,6 +77,8 @@ const app = express();
|
|||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
// serve files inside the public folder from /
|
// serve files inside the public folder from /
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
app.use('/', express.static(path.join(__dirname, 'public')));
|
app.use('/', express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
app.get('/', async (req, res) => {
|
app.get('/', async (req, res) => {
|
||||||
@ -665,7 +668,7 @@ app.get('/api/v1/stats/hardware-models', async (req, res) => {
|
|||||||
return {
|
return {
|
||||||
count: result._count.hardware_model,
|
count: result._count.hardware_model,
|
||||||
hardware_model: result.hardware_model,
|
hardware_model: result.hardware_model,
|
||||||
hardware_model_name: HardwareModel.valuesById[result.hardware_model] ?? "UNKNOWN",
|
hardware_model_name: HardwareModel[result.hardware_model] ?? "UNKNOWN",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
|
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
|
||||||
"@prisma/client": "^5.11.0",
|
"@prisma/client": "^5.11.0",
|
||||||
|
Reference in New Issue
Block a user