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
|
||||
uses: actions/checkout@v4
|
||||
- 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
|
||||
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
|
||||
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
|
||||
run: |
|
||||
docker push git.arinity.org/ctmesh/map:latest
|
||||
|
@ -6,7 +6,7 @@ services:
|
||||
depends_on:
|
||||
database:
|
||||
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:
|
||||
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"
|
||||
}
|
||||
}
|
1268
mqtt/index.js
1268
mqtt/index.js
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@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
|
||||
@ -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.
|
||||
@ -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",
|
||||
"install": "^0.13.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-arrowheads": "^1.4.0",
|
||||
"leaflet-geometryutil": "^0.10.3",
|
||||
"leaflet-groupedlayercontrol": "^0.6.1",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"moment": "^2.30.1",
|
||||
@ -2700,6 +2702,25 @@
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"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": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/leaflet-groupedlayercontrol/-/leaflet-groupedlayercontrol-0.6.1.tgz",
|
||||
|
@ -16,6 +16,8 @@
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"install": "^0.13.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-arrowheads": "^1.4.0",
|
||||
"leaflet-geometryutil": "^0.10.3",
|
||||
"leaflet-groupedlayercontrol": "^0.6.1",
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"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>
|
||||
const emit = defineEmits(['showTraceRoute']);
|
||||
import moment from 'moment';
|
||||
import { state } from '../../store.js';
|
||||
import { findNodeById } from '../../utils.js';
|
||||
</script>
|
||||
|
@ -64,7 +64,7 @@ import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from
|
||||
</div>
|
||||
<div class="my-auto relative flex flex-none items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
@ -83,7 +83,7 @@ import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from
|
||||
</div>
|
||||
<div class="my-auto relative flex flex-none items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
@ -102,7 +102,7 @@ import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from
|
||||
</div>
|
||||
<div class="my-auto relative flex flex-none items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
@ -121,7 +121,7 @@ import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from
|
||||
</div>
|
||||
<div class="my-auto relative flex flex-none items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
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-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
|
||||
import {
|
||||
|
@ -11,8 +11,11 @@ import Announcement from '../components/Announcement.vue';
|
||||
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import L from 'leaflet/dist/leaflet.js';
|
||||
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
|
||||
import 'leaflet/dist/leaflet';
|
||||
const L = window.L;
|
||||
import 'leaflet-geometryutil';
|
||||
import 'leaflet-arrowheads';
|
||||
import 'leaflet.markercluster';
|
||||
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
|
||||
import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue';
|
||||
import { state } from '../store.js';
|
||||
@ -298,6 +301,10 @@ function onNodesUpdated(nodes) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// add node to cache
|
||||
state.nodes.push(node);
|
||||
|
||||
// skip nodes without position
|
||||
if (!node.latitude || !node.longitude) {
|
||||
continue;
|
||||
@ -322,7 +329,7 @@ function onNodesUpdated(nodes) {
|
||||
let icon = icons.mqttDisconnected;
|
||||
|
||||
// use offline icon for nodes older than configured node offline age
|
||||
if (nodesOfflineAge) {
|
||||
if (nodesOfflineAge.value) {
|
||||
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
||||
if (lastUpdatedAgeInMillis > nodesOfflineAge.value * 1000) {
|
||||
icon = icons.offline;
|
||||
@ -365,8 +372,7 @@ function onNodesUpdated(nodes) {
|
||||
});
|
||||
}
|
||||
|
||||
// Push node and marker to cache
|
||||
state.nodes.push(node);
|
||||
// Push node marker to cache
|
||||
state.nodeMarkers[node.node_id] = marker;
|
||||
|
||||
// show node info tooltip when clicking node marker
|
||||
|
@ -1,14 +1,15 @@
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
const commandLineArgs = require("command-line-args");
|
||||
const commandLineUsage = require("command-line-usage");
|
||||
import path from "path";
|
||||
import { fileURLToPath } from 'url';
|
||||
import express from "express";
|
||||
import compression from "compression";
|
||||
import commandLineArgs from "command-line-args";
|
||||
import commandLineUsage from "command-line-usage";
|
||||
|
||||
// protobuf imports
|
||||
const { Mesh, Config } = require("@meshtastic/protobufs");
|
||||
import { Mesh, Config } from "@meshtastic/protobufs";
|
||||
|
||||
// create prisma db client
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// return big ints as string when using JSON.stringify
|
||||
@ -46,7 +47,7 @@ if(options.help){
|
||||
},
|
||||
]);
|
||||
console.log(usage);
|
||||
return;
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// get options and fallback to default values
|
||||
@ -76,6 +77,8 @@ const app = express();
|
||||
app.use(compression());
|
||||
|
||||
// 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.get('/', async (req, res) => {
|
||||
@ -665,7 +668,7 @@ app.get('/api/v1/stats/hardware-models', async (req, res) => {
|
||||
return {
|
||||
count: result._count.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",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
|
||||
"@prisma/client": "^5.11.0",
|
||||
|
Reference in New Issue
Block a user