203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
const crypto = require("crypto");
|
|
const path = require("path");
|
|
const mqtt = require("mqtt");
|
|
const protobufjs = require("protobufjs");
|
|
|
|
// create prisma db client
|
|
const { PrismaClient } = require("@prisma/client");
|
|
const prisma = new PrismaClient();
|
|
|
|
// create mqtt client
|
|
const client = mqtt.connect("mqtt://mqtt.meshtastic.org", {
|
|
username: "meshdev",
|
|
password: "large4cats",
|
|
});
|
|
|
|
// load protobufs
|
|
const root = new protobufjs.Root();
|
|
root.resolvePath = (origin, target) => path.join(__dirname, "protos", target);
|
|
root.loadSync('meshtastic/mqtt.proto');
|
|
const Data = root.lookupType("Data");
|
|
const ServiceEnvelope = root.lookupType("ServiceEnvelope");
|
|
const Position = root.lookupType("Position");
|
|
const Telemetry = root.lookupType("Telemetry");
|
|
const User = root.lookupType("User");
|
|
|
|
function createNonce(packetId, fromNode) {
|
|
|
|
// Expand packetId to 64 bits
|
|
const packetId64 = BigInt(packetId);
|
|
|
|
// Initialize block counter (32-bit, starts at zero)
|
|
const blockCounter = 0;
|
|
|
|
// Create a buffer for the nonce
|
|
const buf = Buffer.alloc(16);
|
|
|
|
// Write packetId, fromNode, and block counter to the buffer
|
|
buf.writeBigUInt64LE(packetId64, 0);
|
|
buf.writeUInt32LE(fromNode, 8);
|
|
buf.writeUInt32LE(blockCounter, 12);
|
|
|
|
return buf;
|
|
|
|
}
|
|
|
|
/**
|
|
* References:
|
|
* https://github.com/crypto-smoke/meshtastic-go/blob/develop/radio/aes.go#L42
|
|
* https://github.com/pdxlocations/Meshtastic-MQTT-Connect/blob/main/meshtastic-mqtt-connect.py#L381
|
|
*/
|
|
function decrypt(packet) {
|
|
try {
|
|
|
|
// default encryption key
|
|
const key = Buffer.from("1PG7OiApB1nwvP+rz05pAQ==", "base64");
|
|
|
|
// create decryption iv/nonce for this packet
|
|
const nonceBuffer = createNonce(packet.id, packet.from);
|
|
|
|
// create aes-128-ctr decipher
|
|
const decipher = crypto.createDecipheriv('aes-128-ctr', key, nonceBuffer);
|
|
|
|
// decrypt encrypted packet
|
|
const decryptedBuffer = Buffer.concat([decipher.update(packet.encrypted), decipher.final()]);
|
|
|
|
// parse as data message
|
|
return Data.decode(decryptedBuffer);
|
|
|
|
} catch(e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// subscribe to everything when connected
|
|
client.on("connect", () => {
|
|
client.subscribe("#");
|
|
});
|
|
|
|
// handle message received
|
|
client.on("message", async (topic, message) => {
|
|
try {
|
|
|
|
// decode service envelope
|
|
const envelope = ServiceEnvelope.decode(message);
|
|
|
|
// attempt to decrypt encrypted packets
|
|
const isEncrypted = envelope.packet.encrypted?.length > 0;
|
|
if(isEncrypted){
|
|
const decoded = decrypt(envelope.packet);
|
|
if(decoded){
|
|
envelope.packet.decoded = decoded;
|
|
}
|
|
}
|
|
|
|
const portnum = envelope.packet?.decoded?.portnum;
|
|
|
|
if(portnum === 3) {
|
|
|
|
const position = Position.decode(envelope.packet.decoded.payload);
|
|
|
|
console.log("POSITION_APP", {
|
|
from: envelope.packet.from.toString(16),
|
|
position: position,
|
|
});
|
|
|
|
// update node position in db
|
|
if(position.latitudeI != null && position.longitudeI){
|
|
try {
|
|
await prisma.node.updateMany({
|
|
where: {
|
|
node_id: envelope.packet.from,
|
|
},
|
|
data: {
|
|
latitude: position.latitudeI,
|
|
longitude: position.longitudeI,
|
|
altitude: position.altitude !== 0 ? position.altitude : null,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if(portnum === 4) {
|
|
|
|
const user = User.decode(envelope.packet.decoded.payload);
|
|
|
|
console.log("NODEINFO_APP", {
|
|
from: envelope.packet.from.toString(16),
|
|
user: user,
|
|
});
|
|
|
|
// create or update node in db
|
|
try {
|
|
await prisma.node.upsert({
|
|
where: {
|
|
node_id: envelope.packet.from,
|
|
},
|
|
create: {
|
|
node_id: envelope.packet.from,
|
|
long_name: user.longName,
|
|
short_name: user.shortName,
|
|
hardware_model: user.hwModel,
|
|
is_licensed: user.isLicensed === true,
|
|
role: user.role,
|
|
},
|
|
update: {
|
|
long_name: user.longName,
|
|
short_name: user.shortName,
|
|
hardware_model: user.hwModel,
|
|
is_licensed: user.isLicensed === true,
|
|
role: user.role,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
}
|
|
|
|
if(portnum === 67) {
|
|
|
|
const telemetry = Telemetry.decode(envelope.packet.decoded.payload);
|
|
|
|
console.log("TELEMETRY_APP", {
|
|
from: envelope.packet.from.toString(16),
|
|
telemetry: telemetry,
|
|
});
|
|
|
|
// data to update
|
|
const data = {};
|
|
|
|
// handle device metrics
|
|
if(telemetry.deviceMetrics){
|
|
data.battery_level = telemetry.deviceMetrics.batteryLevel !== 0 ? telemetry.deviceMetrics.batteryLevel : null;
|
|
data.voltage = telemetry.deviceMetrics.voltage !== 0 ? telemetry.deviceMetrics.voltage : null;
|
|
data.channel_utilization = telemetry.deviceMetrics.channelUtilization !== 0 ? telemetry.deviceMetrics.channelUtilization : null;
|
|
data.air_util_tx = telemetry.deviceMetrics.airUtilTx !== 0 ? telemetry.deviceMetrics.airUtilTx : null;
|
|
}
|
|
|
|
// update node telemetry in db
|
|
if(Object.keys(data).length > 0){
|
|
try {
|
|
await prisma.node.updateMany({
|
|
where: {
|
|
node_id: envelope.packet.from,
|
|
},
|
|
data: data,
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
} catch(e) {
|
|
// ignore errors
|
|
}
|
|
});
|