2024-03-12 18:31:17 +13:00
const path = require ( 'path' ) ;
const express = require ( 'express' ) ;
2024-06-08 16:16:39 +12:00
const compression = require ( 'compression' ) ;
2024-03-13 03:46:50 +13:00
const protobufjs = require ( "protobufjs" ) ;
2024-04-24 16:17:09 +12:00
const commandLineArgs = require ( "command-line-args" ) ;
const commandLineUsage = require ( "command-line-usage" ) ;
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-04-24 16:17:09 +12:00
const optionsList = [
{
name : 'help' ,
alias : 'h' ,
type : Boolean ,
description : 'Display this usage guide.'
} ,
{
name : "port" ,
type : Number ,
description : "Port to serve web ui and api from." ,
} ,
] ;
// parse command line args
const options = commandLineArgs ( optionsList ) ;
// show help
if ( options . help ) {
const usage = commandLineUsage ( [
{
header : 'Meshtastic Map' ,
content : 'A map of all Meshtastic nodes heard via MQTT.' ,
} ,
{
header : 'Options' ,
optionList : optionsList ,
} ,
] ) ;
console . log ( usage ) ;
return ;
}
// get options and fallback to default values
const port = options [ "port" ] ? ? 8080 ;
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-06-08 16:16:39 +12:00
// enable compression
app . use ( compression ( ) ) ;
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 ( {
2024-06-06 23:56:40 +12:00
where : {
node _id : nodeId ,
} ,
2024-03-14 02:39:41 +13:00
} ) ;
// 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-06-06 23:56:40 +12:00
app . get ( '/api/v1/nodes/:nodeId/environment-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" ,
} ) ;
return ;
}
// get latest environment metrics
const environmentMetrics = await prisma . environmentMetric . findMany ( {
where : {
node _id : node . node _id ,
} ,
orderBy : {
id : 'desc' ,
} ,
take : count ,
} ) ;
res . json ( {
environment _metrics : environmentMetrics ,
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
2024-06-07 00:02:57 +12:00
app . get ( '/api/v1/nodes/:nodeId/power-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" ,
} ) ;
return ;
}
// get latest power metrics
const powerMetrics = await prisma . powerMetric . findMany ( {
where : {
node _id : node . node _id ,
} ,
orderBy : {
id : 'desc' ,
} ,
take : count ,
} ) ;
res . json ( {
power _metrics : powerMetrics ,
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
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 ( {
2024-08-09 09:45:55 -07:00
traceroutes : traceroutes . map ( ( trace ) => {
if ( typeof ( trace . route ) === "string" ) {
trace . route = JSON . parse ( trace . route ) ;
}
return trace ;
} ) ,
2024-03-19 02:33:11 +13:00
} ) ;
} catch ( err ) {
console . error ( err ) ;
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
2024-08-04 16:44:01 +02:00
app . get ( '/api/v1/nodes/:nodeId/position-history' , async ( req , res ) => {
try {
2024-08-20 18:47:50 +12:00
// defaults
const nowInMilliseconds = new Date ( ) . getTime ( ) ;
const oneHourAgoInMilliseconds = new Date ( ) . getTime ( ) - ( 3600 * 1000 ) ;
// get request params
2024-08-04 16:44:01 +02:00
const nodeId = parseInt ( req . params . nodeId ) ;
2024-08-20 18:47:50 +12:00
const timeFrom = req . query . time _from ? parseInt ( req . query . time _from ) : oneHourAgoInMilliseconds ;
const timeTo = req . query . time _to ? parseInt ( req . query . time _to ) : nowInMilliseconds ;
2024-08-04 16:44:01 +02:00
2024-08-20 18:47:50 +12:00
// find node
2024-08-04 16:44:01 +02:00
const node = await prisma . node . findFirst ( {
where : {
node _id : nodeId ,
} ,
} ) ;
// make sure node exists
if ( ! node ) {
res . status ( 404 ) . json ( {
message : "Not Found" ,
} ) ;
return ;
}
const positions = await prisma . position . findMany ( {
where : {
node _id : nodeId ,
created _at : {
gte : new Date ( timeFrom ) ,
lte : new Date ( timeTo ) ,
} ,
}
} ) ;
const mapReports = await prisma . mapReport . findMany ( {
where : {
node _id : nodeId ,
created _at : {
gte : new Date ( timeFrom ) ,
lte : new Date ( timeTo ) ,
} ,
}
} ) ;
const positionHistory = [ ]
positions . forEach ( ( position ) => {
positionHistory . push ( {
2024-08-29 00:19:23 +12:00
id : position . id ,
2024-08-04 16:44:01 +02:00
node _id : position . node _id ,
2024-08-29 00:19:23 +12:00
type : "position" ,
2024-08-04 16:44:01 +02:00
latitude : position . latitude ,
longitude : position . longitude ,
altitude : position . altitude ,
2024-08-28 23:54:22 +12:00
gateway _id : position . gateway _id ,
channel _id : position . channel _id ,
2024-08-20 18:47:50 +12:00
created _at : position . created _at ,
2024-08-04 16:44:01 +02:00
} ) ;
} ) ;
mapReports . forEach ( ( mapReport ) => {
positionHistory . push ( {
node _id : mapReport . node _id ,
2024-08-29 00:19:23 +12:00
type : "map_report" ,
2024-08-04 16:44:01 +02:00
latitude : mapReport . latitude ,
longitude : mapReport . longitude ,
altitude : mapReport . altitude ,
2024-08-20 18:47:50 +12:00
created _at : mapReport . created _at ,
2024-08-04 16:44:01 +02:00
} ) ;
} ) ;
2024-08-20 18:47:50 +12:00
// sort oldest to newest
positionHistory . sort ( ( a , b ) => a . created _at - b . created _at ) ;
2024-08-04 16:44:01 +02:00
res . json ( {
position _history : positionHistory ,
} ) ;
} 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-07-05 21:10:35 +12:00
app . get ( '/api/v1/text-messages' , async ( req , res ) => {
try {
// get query params
const to = req . query . to ? ? undefined ;
const from = req . query . from ? ? undefined ;
const channelId = req . query . channel _id ? ? undefined ;
const gatewayId = req . query . gateway _id ? ? undefined ;
2024-07-07 16:43:30 +12:00
const directMessageNodeIds = req . query . direct _message _node _ids ? . split ( "," ) ? ? undefined ;
2024-07-05 23:06:44 +12:00
const lastId = req . query . last _id ? parseInt ( req . query . last _id ) : undefined ;
2024-07-05 21:10:35 +12:00
const count = req . query . count ? parseInt ( req . query . count ) : 50 ;
const order = req . query . order ? ? "asc" ;
2024-07-07 16:43:30 +12:00
// if direct message node ids are provided, there should be exactly two node ids
if ( directMessageNodeIds !== undefined && directMessageNodeIds . length !== 2 ) {
res . status ( 400 ) . json ( {
message : "direct_message_node_ids requires 2 node ids separated by a comma." ,
} ) ;
return ;
}
// default where clauses that should always be used for filtering
var where = {
channel _id : channelId ,
gateway _id : gatewayId ,
// when ordered oldest to newest (asc), only get records after last id
// when ordered newest to oldest (desc), only get records before last id
id : order === "asc" ? {
gt : lastId ,
} : {
lt : lastId ,
} ,
} ;
// if direct message node ids are provided, we expect exactly 2 node ids
if ( directMessageNodeIds !== undefined && directMessageNodeIds . length === 2 ) {
// filter message by "to -> from" or "from -> to"
const [ firstNodeId , secondNodeId ] = directMessageNodeIds ;
where = {
AND : where ,
OR : [
{
to : firstNodeId ,
from : secondNodeId ,
} ,
{
to : secondNodeId ,
from : firstNodeId ,
} ,
] ,
} ;
} else {
// filter by to and from
where = {
... where ,
2024-07-05 21:10:35 +12:00
to : to ,
from : from ,
2024-07-07 16:43:30 +12:00
} ;
}
// get text messages from db
const textMessages = await prisma . textMessage . findMany ( {
where : where ,
2024-07-05 21:10:35 +12:00
orderBy : {
id : order ,
} ,
take : count ,
} ) ;
res . json ( {
text _messages : textMessages ,
} ) ;
} catch ( err ) {
res . status ( 500 ) . json ( {
message : "Something went wrong, try again later." ,
} ) ;
}
} ) ;
2024-07-05 23:06:44 +12:00
app . get ( '/api/v1/text-messages/embed' , async ( req , res ) => {
res . sendFile ( path . join ( _ _dirname , 'public/text-messages-embed.html' ) ) ;
} ) ;
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
2024-04-24 16:17:09 +12:00
const listener = app . listen ( port , ( ) => {
2024-03-23 09:57:31 +13:00
const port = listener . address ( ) . port ;
console . log ( ` Server running at http://127.0.0.1: ${ port } ` ) ;
} ) ;