2024-03-12 18:31:17 +13:00
const path = require ( 'path' ) ;
const express = require ( 'express' ) ;
2024-03-13 03:46:50 +13:00
const protobufjs = require ( "protobufjs" ) ;
2024-03-12 18:31:17 +13:00
2024-03-13 03:46:50 +13:00
// create prisma db client
const { PrismaClient } = require ( "@prisma/client" ) ;
const prisma = new PrismaClient ( ) ;
2024-03-12 18:31:17 +13:00
2024-03-13 03:46:50 +13:00
// return big ints as string when using JSON.stringify
BigInt . prototype . toJSON = function ( ) {
return this . toString ( ) ;
}
2024-03-12 18:31:17 +13:00
2024-03-13 03:46:50 +13:00
// load protobufs
const root = new protobufjs . Root ( ) ;
root . resolvePath = ( origin , target ) => path . join ( _ _dirname , "protos" , target ) ;
root . loadSync ( 'meshtastic/mqtt.proto' ) ;
const HardwareModel = root . lookupEnum ( "HardwareModel" ) ;
const Role = root . lookupEnum ( "Config.DeviceConfig.Role" ) ;
2024-04-01 00:05:44 +13:00
const RegionCode = root . lookupEnum ( "Config.LoRaConfig.RegionCode" ) ;
const ModemPreset = root . lookupEnum ( "Config.LoRaConfig.ModemPreset" ) ;
2024-03-12 18:31:17 +13:00
2024-03-24 00:14:40 +13:00
// appends extra info for node objects returned from api
function formatNodeInfo ( node ) {
return {
... node ,
node _id _hex : "!" + node . node _id . toString ( 16 ) ,
2024-04-01 00:07:47 +13:00
hardware _model _name : HardwareModel . valuesById [ node . hardware _model ] ? ? null ,
role _name : Role . valuesById [ node . role ] ? ? null ,
region _name : RegionCode . valuesById [ node . region ] ? ? null ,
modem _preset _name : ModemPreset . valuesById [ node . modem _preset ] ? ? null ,
2024-03-24 00:14:40 +13:00
} ;
}
2024-03-13 03:46:50 +13:00
const app = express ( ) ;
2024-03-12 18:31:17 +13:00
2024-03-13 03:46:50 +13:00
// serve files inside the public folder from /
app . use ( '/' , express . static ( path . join ( _ _dirname , 'public' ) ) ) ;
2024-03-12 18:31:17 +13:00
app . get ( '/' , async ( req , res ) => {
res . sendFile ( path . join ( _ _dirname , 'public/index.html' ) ) ;
} ) ;
app . get ( '/api' , async ( req , res ) => {
const links = [
{
"path" : "/api" ,
"description" : "This page" ,
} ,
{
"path" : "/api/v1/nodes" ,
"description" : "Meshtastic nodes in JSON format." ,
} ,
2024-03-24 00:22:48 +13:00
{
"path" : "/api/v1/stats/hardware-models" ,
"description" : "Database statistics about hardware models in JSON format." ,
} ,
2024-03-24 00:20:16 +13:00
{
"path" : "/api/v1/waypoints" ,
"description" : "Meshtastic waypoints in JSON format." ,
} ,
2024-03-12 18:31:17 +13:00
] ;
const html = links . map ( ( link ) => {
return ` <li><a href=" ${ link . path } "> ${ link . path } </a> - ${ link . description } </li> ` ;
} ) . join ( "" ) ;
res . send ( html ) ;
} ) ;
app . get ( '/api/v1/nodes' , async ( req , res ) => {
try {
2024-03-13 03:46:50 +13:00
// get nodes from db
const nodes = await prisma . node . findMany ( ) ;
2024-03-12 18:31:17 +13:00
2024-03-24 00:14:40 +13:00
const nodesWithInfo = [ ] ;
2024-03-13 14:03:00 +13:00
for ( const node of nodes ) {
2024-03-24 00:14:40 +13:00
nodesWithInfo . push ( formatNodeInfo ( node ) ) ;
}
res . json ( {
nodes : nodesWithInfo ,
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
app . get ( '/api/v1/nodes/:nodeId' , async ( req , res ) => {
try {
const nodeId = parseInt ( req . params . nodeId ) ;
// find node
const node = await prisma . node . findFirst ( {
where : {
node _id : nodeId ,
} ,
} ) ;
// make sure node exists
if ( ! node ) {
res . status ( 404 ) . json ( {
message : "Not Found" ,
} ) ;
return ;
2024-03-13 14:03:00 +13:00
}
2024-03-12 18:31:17 +13:00
res . json ( {
2024-03-24 00:14:40 +13:00
node : formatNodeInfo ( node ) ,
2024-03-12 18:31:17 +13:00
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
2024-03-14 02:39:41 +13:00
} ) ;
}
} ) ;
app . get ( '/api/v1/nodes/:nodeId/device-metrics' , async ( req , res ) => {
try {
const nodeId = parseInt ( req . params . nodeId ) ;
const count = req . query . count ? parseInt ( req . query . count ) : undefined ;
// find node
const node = await prisma . node . findFirst ( {
where : {
node _id : nodeId ,
} ,
} ) ;
// make sure node exists
if ( ! node ) {
res . status ( 404 ) . json ( {
message : "Not Found" ,
} ) ;
2024-03-24 00:14:40 +13:00
return ;
2024-03-14 02:39:41 +13:00
}
// get latest device metrics
const deviceMetrics = await prisma . deviceMetric . findMany ( {
where : {
node _id : node . node _id ,
} ,
orderBy : {
id : 'desc' ,
} ,
take : count ,
} ) ;
res . json ( {
device _metrics : deviceMetrics ,
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
2024-03-16 19:07:02 +13:00
} ) ;
}
} ) ;
2024-03-16 19:29:58 +13:00
app . get ( '/api/v1/nodes/:nodeId/mqtt-metrics' , async ( req , res ) => {
2024-03-16 19:07:02 +13:00
try {
const nodeId = parseInt ( req . params . nodeId ) ;
// find node
const node = await prisma . node . findFirst ( {
where : {
node _id : nodeId ,
} ,
} ) ;
// make sure node exists
if ( ! node ) {
res . status ( 404 ) . json ( {
message : "Not Found" ,
} ) ;
2024-03-24 00:14:40 +13:00
return ;
2024-03-16 19:07:02 +13:00
}
2024-03-16 19:29:58 +13:00
// get mqtt topics published to by this node
const queryResult = await prisma . $queryRaw ` select mqtt_topic, count(*) as packet_count, max(created_at) as last_packet_at from service_envelopes where gateway_id = ${ nodeId } group by mqtt_topic order by packet_count desc; ` ;
2024-03-16 19:07:02 +13:00
res . json ( {
2024-03-16 19:29:58 +13:00
mqtt _metrics : queryResult ,
2024-03-16 19:07:02 +13:00
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
2024-03-12 18:31:17 +13:00
} ) ;
}
} ) ;
2024-04-05 17:50:04 +13:00
app . get ( '/api/v1/nodes/:nodeId/neighbours' , async ( req , res ) => {
try {
const nodeId = parseInt ( req . params . nodeId ) ;
// find node
const node = await prisma . node . findFirst ( {
where : {
node _id : nodeId ,
} ,
} ) ;
// make sure node exists
if ( ! node ) {
res . status ( 404 ) . json ( {
message : "Not Found" ,
} ) ;
return ;
}
// get nodes from db that have this node as a neighbour
const nodesThatHeardUs = await prisma . node . findMany ( {
where : {
neighbours : {
array _contains : {
node _id : Number ( nodeId ) ,
} ,
} ,
} ,
} ) ;
res . json ( {
nodes _that _we _heard : node . neighbours . map ( ( neighbour ) => {
return {
... neighbour ,
updated _at : node . neighbours _updated _at ,
} ;
} ) ,
nodes _that _heard _us : nodesThatHeardUs . map ( ( nodeThatHeardUs ) => {
const neighbourInfo = nodeThatHeardUs . neighbours . find ( ( neighbour ) => neighbour . node _id . toString ( ) === node . node _id . toString ( ) ) ;
return {
node _id : Number ( nodeThatHeardUs . node _id ) ,
snr : neighbourInfo . snr ,
updated _at : nodeThatHeardUs . neighbours _updated _at ,
} ;
} ) ,
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
2024-03-19 02:33:11 +13:00
app . get ( '/api/v1/nodes/:nodeId/traceroutes' , async ( req , res ) => {
try {
const nodeId = parseInt ( req . params . nodeId ) ;
const count = req . query . count ? parseInt ( req . query . count ) : 10 ; // can't set to null because of $queryRaw
// find node
const node = await prisma . node . findFirst ( {
where : {
node _id : nodeId ,
} ,
} ) ;
// make sure node exists
if ( ! node ) {
res . status ( 404 ) . json ( {
message : "Not Found" ,
} ) ;
2024-03-24 00:14:40 +13:00
return ;
2024-03-19 02:33:11 +13:00
}
// get latest traceroutes
2024-04-15 23:41:16 +01:00
// We want replies where want_response is false and it will be "to" the
// requester.
2024-04-16 14:15:25 +12:00
const traceroutes = await prisma . $queryRaw ` SELECT * FROM traceroutes WHERE want_response = false and \` to \` = ${ node . node _id } and gateway_id is not null order by id desc limit ${ count } ` ;
2024-03-19 02:33:11 +13:00
res . json ( {
traceroutes : traceroutes ,
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
2024-03-13 21:02:58 +13:00
app . get ( '/api/v1/stats/hardware-models' , async ( req , res ) => {
try {
// get nodes from db
const results = await prisma . node . groupBy ( {
by : [ 'hardware_model' ] ,
orderBy : {
_count : {
hardware _model : 'desc' ,
} ,
} ,
_count : {
hardware _model : true ,
} ,
} ) ;
const hardwareModelStats = results . map ( ( result ) => {
return {
count : result . _count . hardware _model ,
hardware _model : result . hardware _model ,
hardware _model _name : HardwareModel . valuesById [ result . hardware _model ] ? ? "UNKNOWN" ,
} ;
} ) ;
res . json ( {
hardware _model _stats : hardwareModelStats ,
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
2024-03-18 10:12:47 +13:00
app . get ( '/api/v1/waypoints' , async ( req , res ) => {
try {
2024-04-05 15:00:15 +13:00
// get waypoints from db
2024-03-18 10:12:47 +13:00
const waypoints = await prisma . waypoint . findMany ( {
orderBy : {
id : 'desc' ,
} ,
} ) ;
// ensure we only have the latest unique waypoints
// since ordered by newest first, older entries will be ignored
const uniqueWaypoints = [ ] ;
for ( const waypoint of waypoints ) {
// skip if we already have a newer entry for this waypoint
if ( uniqueWaypoints . find ( ( w ) => w . from === waypoint . from && w . waypoint _id === waypoint . waypoint _id ) ) {
continue ;
}
// first time seeing this waypoint, add to unique list
uniqueWaypoints . push ( waypoint ) ;
}
2024-04-05 15:00:15 +13:00
// we only want waypoints that haven't expired yet
const nonExpiredWayPoints = uniqueWaypoints . filter ( ( waypoint ) => {
const nowInSeconds = Math . floor ( Date . now ( ) / 1000 ) ;
return waypoint . expire >= nowInSeconds ;
} ) ;
2024-03-18 10:12:47 +13:00
res . json ( {
2024-04-05 15:00:15 +13:00
waypoints : nonExpiredWayPoints ,
2024-03-18 10:12:47 +13:00
} ) ;
} catch ( err ) {
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
2024-03-23 09:57:31 +13:00
// start express server
const listener = app . listen ( 8080 , ( ) => {
const port = listener . address ( ) . port ;
console . log ( ` Server running at http://127.0.0.1: ${ port } ` ) ;
} ) ;