Compare commits

18 Commits

Author SHA1 Message Date
2f76091ae6 fix nodeinfo and telemetry
All checks were successful
Build Docker containers / Build (push) Successful in 42s
2025-04-18 13:06:11 +00:00
0f4035ec3a add debug-incoming-packets option
All checks were successful
Build Docker containers / Build (push) Successful in 42s
2025-04-17 14:22:37 +00:00
18df989fc6 fix markercluster issues
All checks were successful
Build Docker containers / Build (push) Successful in 40s
2025-04-16 01:49:20 -04:00
e694ffa66a remove extra )
All checks were successful
Build Docker containers / Build (push) Successful in 40s
2025-04-16 00:54:04 -04:00
da99bfdeef add arrowheads plugin 2025-04-16 00:53:54 -04:00
b2a9488efd add back text-message-embed
Some checks failed
Build Docker containers / Build (push) Failing after 10s
2025-04-16 00:50:27 -04:00
6b7149905a fix background colors in traceroute window
Some checks failed
Build Docker containers / Build (push) Failing after 10s
2025-04-16 00:43:50 -04:00
8497053aed fix missing nodes, fix issue with traceroutes
All checks were successful
Build Docker containers / Build (push) Successful in 44s
2025-04-16 00:37:24 -04:00
bfdd6cba22 fix nodes all showing offline
All checks were successful
Build Docker containers / Build (push) Successful in 42s
2025-04-16 00:30:03 -04:00
6554d270bc fix call to HardwareInfo
All checks were successful
Build Docker containers / Build (push) Successful in 39s
2025-04-16 00:28:05 -04:00
bcaa1f2c20 fix typo in neighbor-info
All checks were successful
Build Docker containers / Build (push) Successful in 49s
2025-04-16 00:23:09 -04:00
0a7e456173 disable cache when building
All checks were successful
Build Docker containers / Build (push) Successful in 42s
2025-04-16 00:17:40 -04:00
d50fe75759 use ES modules
All checks were successful
Build Docker containers / Build (push) Successful in 8s
2025-04-16 00:15:50 -04:00
d963520486 use EM modules
All checks were successful
Build Docker containers / Build (push) Successful in 21s
2025-04-16 00:13:12 -04:00
e025140ab4 switch to ES modules
All checks were successful
Build Docker containers / Build (push) Successful in 47s
2025-04-16 00:08:48 -04:00
e8095fce81 update docker-compose
All checks were successful
Build Docker containers / Build (push) Successful in 17s
2025-04-15 23:48:54 -04:00
af5690e524 rewrite parser for mqtt
All checks were successful
Build Docker containers / Build (push) Successful in 30s
2025-04-15 21:26:45 -04:00
33726de0cb start work on lora ingest app
All checks were successful
Build Docker containers / Build (push) Successful in 16s
2025-04-15 20:16:04 -04:00
21 changed files with 1691 additions and 647 deletions

View File

@ -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

View File

@ -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
View File

@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io

60
lora/LoraStream.js Normal file
View 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
View 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
View 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
View 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
View 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"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -1,4 +1,4 @@
class NodeIdUtil {
export default class NodeIdUtil {
/**
* Converts the provided hex id to a numeric id, for example: !FFFFFFFF to 4294967295
@ -18,6 +18,4 @@ class NodeIdUtil {
}
}
module.exports = NodeIdUtil;
}

View File

@ -1,4 +1,4 @@
class PositionUtil {
export default class PositionUtil {
/**
* Obfuscates the provided latitude or longitude down to the provided precision in bits.
@ -61,6 +61,4 @@ class PositionUtil {
}
}
module.exports = PositionUtil;
}

View File

@ -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",

View File

@ -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",

View 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('<', '&lt;')
.replaceAll('>', '&gt;')
.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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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

View File

@ -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",
};
});

View File

@ -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",