├── .gitignore ├── .gitmodules ├── .replit ├── Dockerfile ├── README.md ├── captain-definition ├── ignoreDB.json ├── index.ts ├── nodeDB.json ├── package-lock.json ├── package.json ├── replit.nix ├── src ├── FifoKeyCache.ts └── MeshPacketQueue.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/protobufs"] 2 | path = src/protobufs 3 | url = https://github.com/meshtastic/protobufs.git 4 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | run = "tsx index.ts" 2 | entrypoint = "index.ts" 3 | hidden = [".config", "package-lock.json", "tsconfig.json"] 4 | modules = ["nodejs-20:v8-20230920-bd784b9"] 5 | 6 | [nix] 7 | channel = "stable-23_11" 8 | 9 | [gitHubImport] 10 | requiredFiles = [".replit", "replit.nix", ".config"] 11 | 12 | [deployment] 13 | run = ["tsx", "index.ts"] 14 | deploymentTarget = "gce" 15 | ignorePorts = true 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | RUN npm install -g tsx 9 | 10 | COPY . . 11 | 12 | RUN git clone https://github.com/meshtastic/protobufs.git src/protobufs 13 | 14 | CMD [ "tsx", "index.ts" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rage Against The Mesh(ine) 2 | a discord bot for meshtastic 3 | 4 | Most of the bot logic is in https://github.com/baymesh/ratm-meshtastic-discord-bot/blob/main/index.ts 5 | 6 | There is a Dockerfile, you can deploy this anywhere that you can run docker containers? 7 | 8 | Or you can set the environment variables and run `tsx index.js` 9 | 10 | ## Enviroment Variables 11 | 12 | | Key | Description | 13 | | ------------- | ------------- | 14 | |`DISCORD_WEBHOOK_URL`| discord webhook url for where to send Bay Mesh messages| 15 | |`SV_DISCORD_WEBHOOK_URL`| discord webhook url for where to send Sac Valley mesh messages| 16 | |`REDIS_ENABLED` | if `true` it we cache in redis, you need to specify the url (see next item)| 17 | |`REDIS_URL`| redis url (with user/pass etc) if you want to have persistent nodeDB| 18 | |`GROUPING_DURATION` | how long the logger will wait for packets for a new message that it sees| 19 | |`PFP_JSON_URL` | json file that links node ids to profile images, example [here](https://raw.githubusercontent.com/baymesh/bot_pfp/refs/heads/main/baymesh_pfp.json)| 20 | -------------------------------------------------------------------------------- /captain-definition: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "dockerfilePath": "./Dockerfile" 4 | } -------------------------------------------------------------------------------- /ignoreDB.json: -------------------------------------------------------------------------------- 1 | ["zzzXXyy"] 2 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import path from "path"; 3 | import mqtt from "mqtt"; 4 | import protobufjs from "protobufjs"; 5 | import fs from "fs"; 6 | import axios from "axios"; 7 | import { fileURLToPath } from "url"; 8 | import { dirname } from "path"; 9 | import FifoKeyCache from "./src/FifoKeyCache"; 10 | import MeshPacketQueue, { PacketGroup } from "./src/MeshPacketQueue"; 11 | import * as Sentry from "@sentry/node"; 12 | import { nodeProfilingIntegration } from "@sentry/profiling-node"; 13 | import { createClient } from "redis"; 14 | import { env } from "process"; 15 | 16 | // generate a pseduo uuid kinda thing to use as an instance id 17 | const INSTANCE_ID = (() => { 18 | return crypto.randomBytes(4).toString("hex"); 19 | })(); 20 | 21 | function loggerDateString() { 22 | return process.env.ENVIRONMENT === "production" 23 | ? "" 24 | : new Date().toISOString() + " "; 25 | } 26 | 27 | const logger = { 28 | info: (message: string) => { 29 | console.log(`${loggerDateString()}[${INSTANCE_ID}] [INFO] ${message}`); 30 | }, 31 | error: (message: string) => { 32 | console.log(`${loggerDateString()}[${INSTANCE_ID}] [ERROR] ${message}`); 33 | }, 34 | debug: (message: string) => { 35 | console.log(`${loggerDateString()}[${INSTANCE_ID}] [DEBUG] ${message}`); 36 | }, 37 | }; 38 | 39 | Sentry.init({ 40 | environment: process.env.ENVIRONMENT || "development", 41 | integrations: [nodeProfilingIntegration()], 42 | // Performance Monitoring 43 | tracesSampleRate: 1.0, // Capture 100% of the transactions 44 | 45 | // Set sampling rate for profiling - this is relative to tracesSampleRate 46 | profilesSampleRate: 1.0, 47 | }); 48 | 49 | Sentry.setTag("instance_id", INSTANCE_ID); 50 | 51 | logger.info(`Starting Rage Against Mesh(ine) ${INSTANCE_ID}`); 52 | 53 | let pfpDb = { default: "https://cdn.discordapp.com/embed/avatars/0.png" }; 54 | 55 | if (process.env.PFP_JSON_URL) { 56 | logger.info(`Using PFP_JSON_URL=${process.env.PFP_JSON_URL}`); 57 | axios.get(process.env.PFP_JSON_URL).then((response) => { 58 | pfpDb = response.data; 59 | logger.info(`Loaded ${Object.keys(pfpDb).length} pfp entries`); 60 | }); 61 | } 62 | 63 | let ignoreDB = JSON.parse(fs.readFileSync("./ignoreDB.json").toString()); 64 | if (process.env.RBL_JSON_URL) { 65 | logger.info(`Using RBL_JSON_URL=${process.env.RBL_JSON_URL}`); 66 | axios.get(process.env.RBL_JSON_URL).then((response) => { 67 | ignoreDB = response.data; 68 | logger.info(`Loaded ${ignoreDB.length} rbl entries`); 69 | }); 70 | } 71 | 72 | const mqttBrokerUrl = "mqtt://mqtt.meshtastic.org"; // the original project took a nose dive, so this server is trash 73 | const basymeshMqttBrokerUrl = "mqtt://mqtt.bayme.sh"; 74 | const mqttUsername = "meshdev"; 75 | const mqttPassword = "large4cats"; 76 | 77 | const redisClient = createClient({ 78 | url: process.env.REDIS_URL, 79 | }); 80 | 81 | (async () => { 82 | if (process.env.REDIS_ENABLED === "true") { 83 | // Connect to redis server 84 | await redisClient.connect(); 85 | logger.info(`Setting active instance id to ${INSTANCE_ID}`); 86 | redisClient.set(`baymesh:active`, INSTANCE_ID); 87 | } 88 | })(); 89 | 90 | const decryptionKeys = [ 91 | "1PG7OiApB1nwvP+rz05pAQ==", // add default "AQ==" decryption key 92 | ]; 93 | 94 | const nodeDB = JSON.parse(fs.readFileSync("./nodeDB.json").toString()); 95 | const cache = new FifoKeyCache(); 96 | const meshPacketQueue = new MeshPacketQueue(); 97 | 98 | const updateNodeDB = ( 99 | node: string, 100 | longName: string, 101 | nodeInfo: any, 102 | hopStart: number, 103 | ) => { 104 | try { 105 | nodeDB[node] = longName; 106 | if (process.env.REDIS_ENABLED === "true") { 107 | redisClient.set(`baymesh:node:${node}`, longName); 108 | const nodeInfoGenericObj = JSON.parse(JSON.stringify(nodeInfo)); 109 | // remove leading "!" from id 110 | nodeInfoGenericObj.id = nodeInfoGenericObj.id.replace("!", ""); 111 | // add hopStart to nodeInfo 112 | nodeInfoGenericObj.hopStart = hopStart; 113 | nodeInfoGenericObj.updatedAt = new Date().getTime(); 114 | redisClient.json 115 | .set(`baymesh:nodeinfo:${node}`, "$", nodeInfoGenericObj) 116 | .then(() => { 117 | // redisClient.json 118 | // .get(`baymesh:nodeinfo:${node}`) // , { path: "$.hwModel" } 119 | // .then((data) => { 120 | // if (data) { 121 | // logger.info(JSON.stringify(data)); 122 | // } 123 | // }); 124 | }) 125 | .catch((err) => { 126 | // console.log(nodeInfoGenericObj); 127 | // if (err === "Error: Existing key has wrong Redis type") { 128 | redisClient.type(`baymesh:nodeinfo:${node}`).then((result) => { 129 | logger.info(result); 130 | if (result === "string") { 131 | redisClient.del(`baymesh:nodeinfo:${node}`).then(() => { 132 | redisClient.json 133 | .set(`baymesh:nodeinfo:${node}`, "$", nodeInfoGenericObj) 134 | .then(() => { 135 | logger.info("deleted and re-added node info for: " + node); 136 | }) 137 | .catch((err) => { 138 | logger.error(err); 139 | }); 140 | }); 141 | } 142 | }); 143 | // } 144 | logger.error(`redis key: baymesh:nodeinfo:${node} ${err}`); 145 | }); 146 | } 147 | fs.writeFileSync( 148 | path.join(__dirname, "./nodeDB.json"), 149 | JSON.stringify(nodeDB, null, 2), 150 | ); 151 | } catch (err) { 152 | // logger.error(err.message); 153 | Sentry.captureException(err); 154 | } 155 | }; 156 | 157 | const isInIgnoreDB = (node: string) => { 158 | return ignoreDB.includes(node); 159 | }; 160 | 161 | const getNodeInfos = async (nodeIds: string[], debug: boolean) => { 162 | try { 163 | // const foo = nodeIds.slice(0, nodeIds.length - 1); 164 | nodeIds = Array.from(new Set(nodeIds)); 165 | const nodeInfos = await redisClient.json.mGet( 166 | nodeIds.map((nodeId) => `baymesh:nodeinfo:${nodeId2hex(nodeId)}`), 167 | "$", 168 | ); 169 | if (debug) { 170 | logger.debug(JSON.stringify(nodeInfos)); 171 | } 172 | 173 | const formattedNodeInfos = nodeInfos.flat().reduce((acc, item) => { 174 | if (item && item.id) { 175 | acc[item.id] = item; 176 | } 177 | return acc; 178 | }, {}); 179 | 180 | // const formattedNodeInfos = nodeInfos.reduce((acc, [info]) => { 181 | // if (info && info.id) { 182 | // acc[info.id] = info; 183 | // } 184 | // return acc; 185 | // }, {}); 186 | if (Object.keys(formattedNodeInfos).length !== nodeIds.length) { 187 | // figure out which nodes are missing from nodeInfo and print them 188 | // console.log( 189 | // "ABC", 190 | // nodeInfos[0].map((nodeInfo) => nodeInfo.id), 191 | // ); 192 | // console.log(Object.keys(formattedNodeInfos).length, nodeIds.length); 193 | const missingNodes = nodeIds.filter((nodeId) => { 194 | return formattedNodeInfos[nodeId] === undefined; 195 | }); 196 | logger.info("Missing nodeInfo for nodes: " + missingNodes.join(",")); 197 | } 198 | // console.log("Feep", nodeInfos); 199 | return formattedNodeInfos; 200 | } catch (err) { 201 | // logger.error(err.message); 202 | Sentry.captureException(err); 203 | } 204 | return {}; 205 | }; 206 | 207 | const getNodeName = (nodeId: string | number) => { 208 | // redisClient.json.get(`baymesh:nodeinfo:${nodeId}`).then((nodeInfo) => { 209 | // if (nodeInfo) { 210 | // logger.info(nodeInfo); 211 | // } 212 | // }); 213 | return nodeDB[nodeId2hex(nodeId)] || "Unknown"; 214 | }; 215 | 216 | const nodeId2hex = (nodeId: string | number) => { 217 | return typeof nodeId === "number" 218 | ? nodeId.toString(16).padStart(8, "0") 219 | : nodeId; 220 | }; 221 | 222 | const nodeHex2id = (nodeHex: string) => { 223 | return parseInt(nodeHex, 16); 224 | }; 225 | 226 | const prettyNodeName = (nodeId: string | number) => { 227 | const nodeIdHex = nodeId2hex(nodeId); 228 | const nodeName = getNodeName(nodeId); 229 | return nodeName ? `${nodeIdHex} - ${nodeName}` : nodeIdHex; 230 | }; 231 | 232 | const __filename = fileURLToPath(import.meta.url); 233 | const __dirname = dirname(__filename); 234 | 235 | // load protobufs 236 | const root = new protobufjs.Root(); 237 | root.resolvePath = (origin, target) => 238 | path.join(__dirname, "src/protobufs", target); 239 | root.loadSync("meshtastic/mqtt.proto"); 240 | const Data = root.lookupType("Data"); 241 | const ServiceEnvelope = root.lookupType("ServiceEnvelope"); 242 | const User = root.lookupType("User"); 243 | const Position = root.lookupType("Position"); 244 | 245 | if (!process.env.DISCORD_WEBHOOK_URL) { 246 | logger.error("DISCORD_WEBHOOK_URL not set"); 247 | process.exit(-1); 248 | } 249 | 250 | const baWebhookUrl = process.env.DISCORD_WEBHOOK_URL; 251 | const baMsWebhookUrl = process.env.DISCORD_MS_WEBOOK_URL; 252 | const svWebhookUrl = process.env.SV_DISCORD_WEBHOOK_URL; 253 | 254 | const mesh_topic = process.env.MQTT_TOPIC || "msh/US/bayarea"; 255 | const grouping_duration = parseInt(process.env.GROUPING_DURATION || "10000"); 256 | 257 | function sendDiscordMessage(webhookUrl: string, payload: any) { 258 | const data = typeof payload === "string" ? { content: payload } : payload; 259 | 260 | return axios 261 | .post(webhookUrl, data) 262 | .then(() => { 263 | // console.log("Message sent successfully"); 264 | }) 265 | .catch((error) => { 266 | logger.error( 267 | `[error] Could not send discord message: ${error.response.status}`, 268 | ); 269 | }); 270 | } 271 | 272 | function processTextMessage(packetGroup: PacketGroup) { 273 | const packet = packetGroup.serviceEnvelopes[0].packet; 274 | const text = packet.decoded.payload.toString(); 275 | logger.debug("createDiscordMessage: " + text); 276 | createDiscordMessage(packetGroup, text); 277 | } 278 | 279 | const createDiscordMessage = async (packetGroup, text) => { 280 | try { 281 | const packet = packetGroup.serviceEnvelopes[0].packet; 282 | const to = nodeId2hex(packet.to); 283 | const from = nodeId2hex(packet.from); 284 | const nodeIdHex = nodeId2hex(from); 285 | 286 | // discard text messages in the form of "seq 6034" "seq 6025" 287 | if (text.match(/^seq \d+$/)) { 288 | return; 289 | } 290 | 291 | if (isInIgnoreDB(from)) { 292 | logger.info( 293 | `MessageId: ${packetGroup.id} Ignoring message from ${prettyNodeName( 294 | from, 295 | )} to ${prettyNodeName(to)} : ${text}`, 296 | ); 297 | return; 298 | } 299 | 300 | // ignore packets older than 5 minutes 301 | if (new Date(packet.rxTime * 1000) < new Date(Date.now() - 5 * 60 * 1000)) { 302 | logger.info( 303 | `MessageId: ${packetGroup.id} Ignoring old message from ${prettyNodeName( 304 | from, 305 | )} to ${prettyNodeName(to)} : ${text}`, 306 | ); 307 | } 308 | 309 | if (process.env.ENVIRONMENT === "production" && to !== "ffffffff") { 310 | logger.info( 311 | `MessageId: ${packetGroup.id} Not to public channel: ${packetGroup.serviceEnvelopes.map((envelope) => envelope.topic)}`, 312 | ); 313 | return; 314 | } 315 | 316 | if ( 317 | packetGroup.serviceEnvelopes.filter((envelope) => 318 | home_topics.some((home_topic) => envelope.topic.startsWith(home_topic)), 319 | ).length === 0 320 | ) { 321 | logger.info( 322 | `MessageId: ${packetGroup.id} No packets found in topic: ${packetGroup.serviceEnvelopes.map((envelope) => envelope.topic)}`, 323 | ); 324 | return; 325 | } 326 | 327 | let nodeInfos = await getNodeInfos( 328 | packetGroup.serviceEnvelopes 329 | .map((se) => se.gatewayId.replace("!", "")) 330 | .concat(from), 331 | false, 332 | ); 333 | 334 | let avatarUrl = pfpDb["default"]; 335 | if (Object.hasOwn(pfpDb, nodeIdHex)) { 336 | avatarUrl = pfpDb[nodeIdHex]; 337 | } 338 | 339 | const maxHopStart = packetGroup.serviceEnvelopes.reduce((acc, se) => { 340 | const hopStart = se.packet.hopStart; 341 | return hopStart > acc ? hopStart : acc; 342 | }, 0); 343 | 344 | // console.log("maxHopStart", maxHopStart); 345 | 346 | const content = { 347 | username: "Mesh Bot", 348 | avatar_url: 349 | "https://cdn.discordapp.com/app-icons/1240017058046152845/295e77bec5f9a44f7311cf8723e9c332.png", 350 | embeds: [ 351 | { 352 | url: `https://meshview.rouvier.org/packet_list/${packet.from}`, 353 | color: 6810260, 354 | timestamp: new Date(packet.rxTime * 1000).toISOString(), 355 | 356 | author: { 357 | name: `${nodeInfos[nodeIdHex] ? nodeInfos[nodeIdHex].longName : "Unknown"}`, 358 | url: `https://meshview.rouvier.org/packet_list/${packet.from}`, 359 | icon_url: avatarUrl, 360 | }, 361 | title: `${nodeInfos[nodeIdHex] ? nodeInfos[nodeIdHex].shortName : "UNK"}`, 362 | description: text, 363 | fields: [ 364 | // { 365 | // name: `${nodeInfos[nodeIdHex] ? nodeInfos[nodeIdHex].shortName : "UNK"}`, 366 | // value: text, 367 | // }, 368 | // { 369 | // name: "Node ID", 370 | // value: `${nodeIdHex}`, 371 | // inline: true, 372 | // }, 373 | { 374 | name: "Packet", 375 | value: `[${packetGroup.id.toString(16)}](https://meshview.rouvier.org/packet/${packetGroup.id})`, 376 | inline: true, 377 | }, 378 | { 379 | name: "Channel", 380 | value: `${packetGroup.serviceEnvelopes[0].channelId}`, 381 | inline: true, 382 | }, 383 | ...packetGroup.serviceEnvelopes 384 | .filter( 385 | (value, index, self) => 386 | self.findIndex((t) => t.gatewayId === value.gatewayId) === 387 | index, 388 | ) 389 | .map((envelope) => { 390 | const gatewayDelay = 391 | envelope.mqttTime.getTime() - packetGroup.time.getTime(); 392 | 393 | if ( 394 | envelope.gatewayId === "!75f1804c" || 395 | envelope.gatewayId === "!3b46b95c" 396 | ) { 397 | // console.log(envelope); 398 | } 399 | 400 | let gatewayDisplaName = envelope.gatewayId.replace("!", ""); 401 | if (nodeInfos[envelope.gatewayId.replace("!", "")]) { 402 | gatewayDisplaName = 403 | // nodeInfos[envelope.gatewayId.replace("!", "")].shortName + 404 | // " - " + 405 | nodeInfos[envelope.gatewayId.replace("!", "")].shortName; //+ 406 | // " " + 407 | // envelope.gatewayId.replace("!", ""); 408 | } 409 | 410 | let hopText = `${envelope.packet.hopStart - envelope.packet.hopLimit}/${envelope.packet.hopStart} hops`; 411 | 412 | if ( 413 | envelope.packet.hopStart === 0 && 414 | envelope.packet.hopLimit === 0 415 | ) { 416 | hopText = `${envelope.packet.rxSnr} / ${envelope.packet.rxRssi} dBm`; 417 | } else if ( 418 | envelope.packet.hopStart - envelope.packet.hopLimit === 419 | 0 420 | ) { 421 | hopText = `${envelope.packet.rxSnr} / ${envelope.packet.rxRssi} dBm ${envelope.packet.hopStart - envelope.packet.hopLimit}/${envelope.packet.hopStart} hops`; 422 | } 423 | 424 | if (envelope.gatewayId.replace("!", "") === nodeIdHex) { 425 | hopText = `Self Gated ${envelope.packet.hopStart} hopper`; 426 | } 427 | 428 | if (maxHopStart !== envelope.packet.hopStart) { 429 | hopText = `:older_man: ${envelope.packet.hopStart - envelope.packet.hopLimit}/${envelope.packet.hopStart} hops`; 430 | } 431 | 432 | if (envelope.mqttServer === "public") { 433 | hopText = `:poop: ${envelope.packet.hopStart - envelope.packet.hopLimit}/${envelope.packet.hopStart} hops`; 434 | } 435 | 436 | return { 437 | name: `Gateway`, 438 | value: `[${gatewayDisplaName} (${hopText})](https://meshview.rouvier.org/packet_list/${nodeHex2id(envelope.gatewayId.replace("!", ""))})${gatewayDelay > 0 ? " (" + gatewayDelay + "ms)" : ""}`, 439 | inline: true, 440 | }; 441 | }), 442 | ], 443 | }, 444 | ], 445 | }; 446 | 447 | //console.log(packetGroup, packetGroup.serviceEnvelopes); 448 | 449 | logger.info( 450 | `MessageId: ${packetGroup.id} Received message from ${prettyNodeName(from)} to ${prettyNodeName(to)} : ${text}`, 451 | ); 452 | 453 | if ( 454 | packetGroup.serviceEnvelopes.filter((envelope) => 455 | ba_home_topics.some((home_topic) => 456 | envelope.topic.startsWith(home_topic), 457 | ), 458 | ).length > 0 459 | ) { 460 | if ( 461 | baMsWebhookUrl && 462 | packetGroup.serviceEnvelopes[0].channelId === "MediumSlow" 463 | ) { 464 | sendDiscordMessage(baMsWebhookUrl, content); 465 | } else { 466 | sendDiscordMessage(baWebhookUrl, content); 467 | } 468 | } 469 | 470 | if ( 471 | packetGroup.serviceEnvelopes.filter((envelope) => 472 | sv_home_topics.some((home_topic) => 473 | envelope.topic.startsWith(home_topic), 474 | ), 475 | ).length > 0 476 | ) { 477 | if (svWebhookUrl) { 478 | sendDiscordMessage(svWebhookUrl, content); 479 | } 480 | } 481 | } catch (err) { 482 | logger.error("Error: " + String(err)); 483 | Sentry.captureException(err); 484 | } 485 | }; 486 | 487 | // const client = mqtt.connect(mqttBrokerUrl, { 488 | // username: mqttUsername, 489 | // password: mqttPassword, 490 | // }); 491 | 492 | const baymesh_client = mqtt.connect(basymeshMqttBrokerUrl, { 493 | username: mqttUsername, 494 | password: mqttPassword, 495 | }); 496 | 497 | const ba_home_topics = [ 498 | "msh/US/bayarea", 499 | "msh/US/BayArea", 500 | "msh/US/CA/bayarea", 501 | "msh/US/CA/BayArea", 502 | ]; 503 | 504 | const sv_home_topics = [ 505 | "msh/US/sacvalley", 506 | "msh/US/SacValley", 507 | "msh/US/CA/sacvalley", 508 | "msh/US/CA/SacValley", 509 | ]; 510 | 511 | // home_topics is both ba and sv 512 | const home_topics = ba_home_topics.concat(sv_home_topics); 513 | 514 | const nodes_to_log_all_positions = [ 515 | "fa6dc348", // me 516 | "3b46b95c", // ohr 517 | "33686ed8", // balloon 518 | ]; 519 | 520 | const subbed_topics = ["msh/US"]; 521 | 522 | // run every 5 seconds and pop off from the queue 523 | const processing_timer = setInterval(() => { 524 | if (process.env.REDIS_ENABLED === "true") { 525 | redisClient.get(`baymesh:active`).then((active_instance) => { 526 | if (active_instance && active_instance !== INSTANCE_ID) { 527 | logger.error( 528 | `Stopping RATM instance; active_instance: ${active_instance} this instance: ${INSTANCE_ID}`, 529 | ); 530 | clearInterval(processing_timer); // do we want to kill it so fast? what about things in the queue? 531 | // subbed_topics.forEach((topic) => client.unsubscribe(topic)); 532 | subbed_topics.forEach((topic) => baymesh_client.unsubscribe(topic)); 533 | } 534 | }); 535 | } 536 | const packetGroups = meshPacketQueue.popPacketGroupsOlderThan( 537 | Date.now() - grouping_duration, 538 | ); 539 | packetGroups.forEach((packetGroup) => { 540 | processPacketGroup(packetGroup); 541 | }); 542 | }, 5000); 543 | 544 | function sub(the_client: mqtt.MqttClient, topic: string) { 545 | the_client.subscribe(`${topic}/#`, (err) => { 546 | if (!err) { 547 | logger.info(`Subscribed to ${topic}/#`); 548 | } else { 549 | logger.error(`Subscription error: ${err.message}`); 550 | } 551 | }); 552 | } 553 | 554 | // subscribe to everything when connected 555 | baymesh_client.on("connect", () => { 556 | logger.info(`Connected to Private MQTT broker`); 557 | subbed_topics.forEach((topic) => sub(baymesh_client, topic)); 558 | }); 559 | 560 | // handle message received 561 | baymesh_client.on("message", async (topic: string, message: any) => { 562 | try { 563 | if (topic.includes("msh")) { 564 | if (!topic.includes("/json")) { 565 | if (topic.includes("/stat/")) { 566 | return; 567 | } 568 | // decode service envelope 569 | let envelope; 570 | try { 571 | envelope = ServiceEnvelope.decode(message); 572 | } catch (envDecodeErr) { 573 | if ( 574 | String(envDecodeErr).indexOf("invalid wire type 7 at offset 1") === 575 | -1 576 | ) { 577 | logger.error( 578 | `MessageId: Error decoding service envelope: ${envDecodeErr}`, 579 | ); 580 | } 581 | return; 582 | } 583 | if (!envelope || !envelope.packet) { 584 | return; 585 | } 586 | 587 | if ( 588 | home_topics.some((home_topic) => topic.startsWith(home_topic)) || 589 | nodes_to_log_all_positions.includes( 590 | nodeId2hex(envelope.packet.from), 591 | ) || 592 | meshPacketQueue.exists(envelope.packet.id) 593 | ) { 594 | // return; 595 | } else { 596 | // logger.info("Message received on topic: " + topic); 597 | return; 598 | } 599 | 600 | // attempt to decrypt encrypted packets 601 | const isEncrypted = envelope.packet.encrypted?.length > 0; 602 | if (isEncrypted) { 603 | const decoded = decrypt(envelope.packet); 604 | if (decoded) { 605 | envelope.packet.decoded = decoded; 606 | } 607 | } 608 | 609 | if (cache.exists(shaHash(envelope))) { 610 | // logger.debug( 611 | // `FifoCache: Already received envelope with hash ${shaHash(envelope)} MessageId: ${envelope.packet.id} Gateway: ${envelope.gatewayId}`, 612 | // ); 613 | return; 614 | } 615 | 616 | if (cache.add(shaHash(envelope))) { 617 | // periodically print the nodeDB to the console 618 | //console.log(JSON.stringify(nodeDB)); 619 | } 620 | 621 | meshPacketQueue.add(envelope, topic, "baymesh"); 622 | } 623 | } 624 | } catch (err) { 625 | logger.error("Error: " + String(err)); 626 | Sentry.captureException(err); 627 | } 628 | }); 629 | 630 | function shaHash(serviceEnvelope: ServiceEnvelope) { 631 | const hash = crypto.createHash("sha256"); 632 | hash.update(JSON.stringify(serviceEnvelope)); 633 | return hash.digest("hex"); 634 | } 635 | 636 | function processPacketGroup(packetGroup: PacketGroup) { 637 | const packet = packetGroup.serviceEnvelopes[0].packet; 638 | const portnum = packet?.decoded?.portnum; 639 | 640 | if (portnum === 1) { 641 | processTextMessage(packetGroup); 642 | } else if (portnum === 3) { 643 | // we used to insert positions in to the postresdb, but no more this is a just a logger 644 | } else if (portnum === 4) { 645 | const user = User.decode(packet.decoded.payload); 646 | const from = nodeId2hex(packet.from); 647 | updateNodeDB(from, user.longName, user, packet.hopStart); 648 | } else { 649 | // logger.debug( 650 | // `MessageId: ${packetGroup.id} Unknown portnum ${portnum} from ${prettyNodeName( 651 | // packet.from, 652 | // )}`, 653 | // ); 654 | } 655 | } 656 | 657 | function createNonce(packetId, fromNode) { 658 | // Expand packetId to 64 bits 659 | const packetId64 = BigInt(packetId); 660 | 661 | // Initialize block counter (32-bit, starts at zero) 662 | const blockCounter = 0; 663 | 664 | // Create a buffer for the nonce 665 | const buf = Buffer.alloc(16); 666 | 667 | // Write packetId, fromNode, and block counter to the buffer 668 | buf.writeBigUInt64LE(packetId64, 0); 669 | buf.writeUInt32LE(fromNode, 8); 670 | buf.writeUInt32LE(blockCounter, 12); 671 | 672 | return buf; 673 | } 674 | 675 | /** 676 | * References: 677 | * https://github.com/crypto-smoke/meshtastic-go/blob/develop/radio/aes.go#L42 678 | * https://github.com/pdxlocations/Meshtastic-MQTT-Connect/blob/main/meshtastic-mqtt-connect.py#L381 679 | */ 680 | function decrypt(packet) { 681 | // attempt to decrypt with all available decryption keys 682 | for (const decryptionKey of decryptionKeys) { 683 | try { 684 | // console.log(`using decryption key: ${decryptionKey}`); 685 | // convert encryption key to buffer 686 | const key = Buffer.from(decryptionKey, "base64"); 687 | 688 | // create decryption iv/nonce for this packet 689 | const nonceBuffer = createNonce(packet.id, packet.from); 690 | 691 | // create aes-128-ctr decipher 692 | const decipher = crypto.createDecipheriv("aes-128-ctr", key, nonceBuffer); 693 | 694 | // decrypt encrypted packet 695 | const decryptedBuffer = Buffer.concat([ 696 | decipher.update(packet.encrypted), 697 | decipher.final(), 698 | ]); 699 | 700 | // parse as data message 701 | return Data.decode(decryptedBuffer); 702 | } catch (e) { 703 | // console.log(e); 704 | } 705 | } 706 | 707 | // couldn't decrypt 708 | return null; 709 | } 710 | -------------------------------------------------------------------------------- /nodeDB.json: -------------------------------------------------------------------------------- 1 | { 2 | "18865324": "Meshtastic 5324", 3 | "25057008": "d4e5-slave-01-DC31-DarknetNG", 4 | "33637000": "interpants", 5 | "33637250": "Mark kf6sml", 6 | "33637780": "Gabe in SF", 7 | "33644078": "Mr_Ks4078", 8 | "33644334": "Turtle 4334", 9 | "33644678": "DUBLIN/Logger", 10 | "33666010": "KnownAs ToWeR ", 11 | "33666640": "Tam Purple", 12 | "33666940": "Hel-Text", 13 | "33676040": "Meshtastic 6040", 14 | "33677424": "JET (Cannon Hill)", 15 | "33678388": "Meshtastic 8388", 16 | "33681670": "Meshtastic 1670", 17 | "33692730": "Corte Madera 🎄 Hill", 18 | "33697370": "Meshtastic 7370", 19 | "33698864": "mesh potatoes ", 20 | "37159041": "K6TRF T-Echo", 21 | "41232815": "SubNet-Trk2", 22 | "43309523": "Meshtastic workshop", 23 | "43560318": "Positron", 24 | "43560428": "KF5RJQ-03", 25 | "43560454": "Secret Diablo", 26 | "43571704": "onyx", 27 | "43577088": "Meshtastic 7088", 28 | "43578250": "N1CKA", 29 | "43589238": "BLUFFS GATEWAY ", 30 | "43590638": "OVW0", 31 | "43596998": "N1CKA", 32 | "49228540": "BLUFFS ECHO NIN", 33 | "70308246": "OVWH", 34 | "74558960": "JJE3 8960", 35 | "82511254": "Petaluma-10", 36 | "84889034": "7 of 915Mhz", 37 | "88564040": "Meshtastic 4040", 38 | "95270060": "dopewars -- DM to play", 39 | "75f1804c": "Formerly Known As OHR [bayme.sh]", 40 | "fa6dc348": "Make A Mesh [bayme.sh]", 41 | "3edfd5a9": "Oakland Hills RAK", 42 | "75f19180": "Meshtastic 9180", 43 | "fa6dc3ac": "Meshed Up [bayme.sh]", 44 | "undefined": "SkyRAK", 45 | "6df76f72": "kroo-techo-01", 46 | "da54346c": "Enorym_346c", 47 | "75f182c4": "Meshtastic 82c4", 48 | "4e9f554b": "BigRak", 49 | "6d087e4c": "MiramarTower 7e4c", 50 | "3e1c8f95": "Big Meshta Stick", 51 | "fa74d32c": "HelltecV3WS not L", 52 | "261ead33": "interregnum", 53 | "f71e47e4": "set infrastructure nodes to 0 or 1 hops", 54 | "e1353816": "ඞRAK", 55 | "ea24668c": "Enorym_668c", 56 | "1c37bfbc": "Meshtastic bfbc", 57 | "e0d2424": "Local Nano", 58 | "778d88b6": "hfidek richmond", 59 | "cb7c6ef7": "Long Name", 60 | "25b1db38": "Visiting frm Houston", 61 | "336a9f1d": "MiramarStatic9f1d", 62 | "1d029c23": "Solar Sensor V3", 63 | "ab2dd99c": "Genentech G1", 64 | "fa672974": "j2", 65 | "f96a8e04": "Meshy-Mobile", 66 | "fa98c5c4": "Button", 67 | "3645e081": "Meshtastic e081", 68 | "e2e2db5c": "ENTS 24", 69 | "ea341854": "W7JHC", 70 | "cb2c6c4b": "Meshtastic 6c4b", 71 | "a1fb268": "solar b268", 72 | "da5edd80": "Meshtastic dd80", 73 | "cf8368de": "TechoEaton", 74 | "6b6c641f": "Meshtastic 641f", 75 | "e0f79ab8": "North Fremont - Lakes&Birds Neigh", 76 | "da545844": "Meshtastic 5844", 77 | "97fa56c7": "termie", 78 | "57865c0a": "Cake Ultra", 79 | "da56f478": "Meshtastic f478", 80 | "d7a51744": "BlueFog1", 81 | "be50a78c": "Albert Filice", 82 | "b07a8220": "Repeater", 83 | "55c73714": "Hill", 84 | "10b72363": "North Beach", 85 | "da543448": "Home", 86 | "f96a9e5c": "Meshtastic 9e5c", 87 | "1fa042a0": "attack decay hold delay", 88 | "79fe9129": "Canard Echo", 89 | "fc165f73": "Meshtastic 5f73", 90 | "ea341af0": "MATTY1AF0", 91 | "da5562e8": "MissScarlet", 92 | "f96a9150": "Meshtastic 9150", 93 | "da63abec": "Rocket Mesh 1", 94 | "bd8a5c50": "RockBerry Mesh 3 🚀", 95 | "da63a53c": "Rocket Mesh 2 🚀 Solar", 96 | "336aa281": "itsdlol", 97 | "fa989780": "Packet Eater 9780", 98 | "2506407c": "Discone", 99 | "3abddb6d": "termie-roam", 100 | "848ab1d3": "Purple", 101 | "fa438818": "DAbelson", 102 | "6d3d8b91": "Hornet", 103 | "da659664": "Terry", 104 | "7c5b00dc": "sirwnstn", 105 | "b791aa09": "Meshtastic aa09", 106 | "1fa06fe0": "Mesh Daddy", 107 | "384f4687": "Pink", 108 | "64bd7dd": "Meshtastic d7dd", 109 | "e0f5d480": "Meshtastic d480", 110 | "75f18044": "Meshtrashtic [bayme.sh]", 111 | "da5ed674": "August mobile ", 112 | "1bc2d9fd": "squid", 113 | "b2479080": "Cake", 114 | "7c5b0968": "jawkDNA", 115 | "75f18048": "K6SH", 116 | "498ae293": "sid", 117 | "da65a36c": "MerlinMesh - Margret", 118 | "7a6bf0b0": "Meshtastic f0b0", 119 | "61e7f80": "username 1", 120 | "da574380": "B-BEAM", 121 | "da659b0c": "Bryan1", 122 | "da55ee2c": "Meshtastic ee2c", 123 | "da53df64": "MerlinMesh - Kilnger", 124 | "da545638": "Meshtastic 5638", 125 | "7c5a5de0": "Alu_Ber", 126 | "dadfaf28": "kroo-tbeam", 127 | "938c0150": "SOMA 0150", 128 | "95421c24": "Meshtastic 1c24", 129 | "a7b0c10": "Rak_Mobile", 130 | "da547aa8": "Meshtastic 7aa8", 131 | "da5cc25c": "Meshtastic c25c", 132 | "da621ba4": "Tom Base - Visiting from Bend OR", 133 | "da628a34": "Rocket Mesh 1 🚀 The Rebirth", 134 | "e0ce2024": "New Kids", 135 | "29597e8": "ElectroBoy", 136 | "da63b950": "Meshtastic b950", 137 | "da5790d0": "ElectroBoy_Port", 138 | "f71e9fd4": "KG6MDW Red", 139 | "ea3419d8": "W5MAH_1", 140 | "ea341100": "W5MAH_1", 141 | "da53e268": "ENORYM_e268", 142 | "7c5a3f88": "KTSN Tail 1 https://bayme.sh", 143 | "971772c": "KI6SSI Router Client", 144 | "df81f5d7": "ShakataGaNai/WCRouter", 145 | "da56f4ec": "mesh7788", 146 | "6f0213ea": "BigRAK", 147 | "7f07bd38": "Meshtastic bd38", 148 | "87a65437": "somebody", 149 | "da565430": "mesh4466", 150 | "336659ac": "Tam Green", 151 | "7c5a597c": "Tam Red", 152 | "7c5a5adc": "Vaca Router", 153 | "7c5a462c": "Muddup-OD", 154 | "d12a5544": "davs2rt 5544", 155 | "e2f22613": "MeshtasticXDSolar https://bayme.sh", 156 | "ea341ab4": "Meshtastic Luckys CV", 157 | "7c5b478c": "BkNeko-02", 158 | "da546308": "N6OIM RV Solar -6308", 159 | "7c5cc588": "Rocket Mesh 4 🚀 Rover", 160 | "33df6611": "Rocket RAK 🚀 Solar Lander", 161 | "599a43e6": "roens 🚗", 162 | "7c5cba10": "KG6MDW Roof Router", 163 | "da53df14": "Meshtastic df14", 164 | "492004d6": "Tracy Hills Repeater", 165 | "1f9ff0e0": "Pony Express Mail", 166 | "92091ba5": "Hex_Joe", 167 | "162dc7fd": "JA-TR-COOKIE", 168 | "da6566c4": "Merlin Mesh - Hunnicutt", 169 | "e09c9293": "Pine Grove Router (JB)", 170 | "da5769a8": "mobilewine", 171 | "a4df434d": "Borked", 172 | "3368ea00": "Meshtastic ea00", 173 | "50c2ae4": "roens tbeam", 174 | "335d8ecc": "BkNeko-01", 175 | "2c49adf3": "JB Romeo", 176 | "d91ffea7": "SubNet-Sol6", 177 | "e33bc900": "Electric Unicycle mobile, N6OIM/3", 178 | "f10fa592": "jfred1 (SacValleyMesh.com)", 179 | "9540503a": "davs3rt 503a", 180 | "36c07a88": "📡 SacValley Bluffs ", 181 | "da53e07c": "MerlinMesh - Hawkeye", 182 | "da65b280": "DA Mobile ", 183 | "e2e26854": "Canard Base - https://bayme.sh", 184 | "a3fa8a54": "KF5RJQ-01", 185 | "180c31e3": "JB Tracker", 186 | "365e4af": "SC base 1", 187 | "60063f6b": "DUBLIN-GW-2 - https://bayme.sh", 188 | "4d6119b7": "HT Brown 🚑", 189 | "da659398": "Spring lake", 190 | "e0d03240": "SR Mesh SR1", 191 | "55bb16e9": "Santa Rosa Mesh", 192 | "7c5a5868": "Bennett Valley SR6", 193 | "7c5cba68": "SR Mesh SR5", 194 | "da6289f8": "SR mobile ", 195 | "3b279a54": " Mount Hood SRW1", 196 | "c0bc2f63": "MT Saint Helena MSH1", 197 | "e2e60024": "ShakataGaNai/TS1", 198 | "2ddc561b": "Meshtastic 561b", 199 | "fabae8ec": "Fennec Mobile", 200 | "59c33145": "KitKat", 201 | "7c5ad334": "Ben Home - ben@N6BRL.com", 202 | "a3dd8e27": "Meshtastic 8e27", 203 | "e2e69468": "Bluffs 1-2", 204 | "da565238": "Fair Oaks East OIM/R", 205 | "1fa06474": "South Fremont Hills", 206 | "da638dd4": "JB Gateway", 207 | "4355f528": "EDH Router (KevinE, https://svme.sh)", 208 | "66a5322f": "Meshtastic 322f", 209 | "33666b38": "Known As ToWeR. Bayme.sh", 210 | "250572f8": "SMRN-C-TBeam-Folsom-Bjarne", 211 | "b9d0a965": "Pine Grove Router (JB)", 212 | "1c5f5864": "Rick Black 5864", 213 | "14fa1e88": "K3M 04", 214 | "22d6db03": "Taylor Mountain W4", 215 | "e89f55eb": "BB01 Home 🇺🇸", 216 | "8ad1c4c": "Luppy-99", 217 | "7a6bf0d0": "Meshtastic f0d0", 218 | "da639058": "SMRN-SS-Ca-Solar sacvalleymesh.com", 219 | "e076cacc": "MeshNet (MN02)", 220 | "d2a5cb28": "BB05 Remote 🏴‍☠️", 221 | "eb93aab5": "Turlock Mesh #1 centralvalleymesh.net", 222 | "a1d279e8": "MeshNet (MN01)", 223 | "336450c4": "Pismo-GW", 224 | "4b50b916": "TeeWest Base1 WRVJ365", 225 | "1f9ffcfc": "SchmacTastic-1", 226 | "55c7ff64": "armbeam", 227 | "398312f5": "TKb Cupertino base", 228 | "da64abb0": "RPN Relay", 229 | "33696dac": "Meshtastic Worth Ct", 230 | "a1021b43": "MeshNet (MN02)", 231 | "7f6310f4": "Mesh10f4", 232 | "b29dac3e": "🍪👹 Router", 233 | "7a6d6220": "🍪👹 Home Assistant", 234 | "da574630": "STARLORD", 235 | "da53f0a8": "Sebastopol Mesh", 236 | "4be333c9": "spacecowboy https://bayme.sh", 237 | "da565298": "Meshtastic 5298", 238 | "af271509": "MeshNet (MN03) - t.ly/meshnet", 239 | "1fa06b84": "MeshtasticXD1", 240 | "c398b08": "Bear-Bear", 241 | "dc37bcb9": "AHRC", 242 | "706a8e54": "The Perch", 243 | "487ac916": "Zender", 244 | "be5c5b31": "Zender/TE", 245 | "3364cc60": "DUBLIN-GW-2 - https://bayme.sh", 246 | "922037cf": "Meshtastic Valentine", 247 | "e2e38e18": "Meshcus ", 248 | "1fa05010": "Meshtastic 5010", 249 | "99e462c4": "Cotati Router - https://armooo.net/m", 250 | "336772fc": "KF5RJQ-04", 251 | "4355ec64": "Landing2", 252 | "3369cfbc": "Puni-Mesher", 253 | "da4c7c17": "SacValleyMesh.com Router-01", 254 | "7c5a4088": "SMRN-RC-Heltec3-Folsom", 255 | "da574cf0": "JJH", 256 | "ea86e9b5": "Tiger e9b5 (Bayme.sh Discord)", 257 | "7c5cc3c8": "(Old) G Base Station", 258 | "919de5b8": "Santa Clara Logger", 259 | "999751e3": "G Mobile", 260 | "f53f441f": "Ko6cnt Base https://bayme.sh", 261 | "d61330af": "Pewter", 262 | "7c5b0354": "Taiwania 0354 🇺🇸🇹🇼", 263 | "a0f494e": "Black Cat Base", 264 | "be1293ac": "Bunny 93ac (Bayme.sh Discord)", 265 | "435623e4": "Papi", 266 | "da9775b8": "d4e5_dc31_darknetNG", 267 | "b9c6816d": "Zoo 816d", 268 | "33676d18": "Robertsville A", 269 | "bcc7f14c": "SC Portable 1", 270 | "7c7a73d5": "Cramster Solar Node (K6JOE)", 271 | "318ef2b8": "JB Relay", 272 | "6fc92954": "TKp1 Cupertino", 273 | "a1bac978": "Meshtastic c978", 274 | "da5ad9bc": "Mesh-ure [bayme.sh]", 275 | "da973ac4": "cr4mb0", 276 | "ebe97dfa": "Pete test client", 277 | "e2e37844": "Meshtastic 7844", 278 | "da97398c": "Vineyard Test Router - 01", 279 | "e7a5c07a": "MotoGPS", 280 | "7efef7dc": "Yao Auto Repair", 281 | "da65b11c": "Node0 SantaCruzMesh", 282 | "3368829c": "🎯Tracker SantaCruzMesh ", 283 | "621593c3": "MeshNet (MN10) - t.ly/meshnet", 284 | "57dea473": "Mesh Net (MN15) ", 285 | "6605468b": "eric@w6hs.net", 286 | "7c5b2ccc": "Taiwania 2ccc", 287 | "a1a5f5ed": "Berryessa Router Temp", 288 | "eecfe349": "Jim mobile", 289 | "55f46fd": "ANT1", 290 | "c177aec": "t-deck", 291 | "7c5a59cc": "VishFish Mobile", 292 | "7c5cbf2c": "T Star Mobile", 293 | "3369dc28": "Kilo Home (Burlingame, CA)", 294 | "35ccb082": "Michifornian", 295 | "bd4670f0": "Floor", 296 | "1c16c5b8": "Fort Router", 297 | "da56e8fc": "Meshtastic e8fc", 298 | "eb91440a": "Meshtastic 440a", 299 | "62dd7a14": "Meshtastic 7a14", 300 | "473f8b47": "K6BEZ base", 301 | "dadfd174": "Meshtastic d174", 302 | "a9d33d66": "BB07 Mobile 🏴‍☠️", 303 | "1f9fec90": "Meshtastic ec90", 304 | "bd467990": "Tower", 305 | "ed74eee7": "RPN2 Relay", 306 | "55c6e164": "Meshtastic e164", 307 | "1fa06a38": "Meshtastic 6a38", 308 | "3b388897": "Terminal-02", 309 | "5f52e43": "Meshtastic 2e43", 310 | "33696f44": "W6HS-2", 311 | "c1a8d64": "t-deck two", 312 | "7c5ac3e4": "Meshtastic c3e4", 313 | "fa6c6290": "bozonet", 314 | "1f99a811": "Meshtastic a811", 315 | "99b61391": "Pablo Base ", 316 | "33635a6c": "SR Capsule C2", 317 | "8abd9e2f": "sleepingWallaby ", 318 | "da569590": "Snape", 319 | "7c5b312c": "K6TRF-Router", 320 | "e33bbc84": "JIMBEAM", 321 | "bbbbc1b0": "Meshtastic c1b0", 322 | "a125d4ca": "Flo", 323 | "335d9148": "HWP Green", 324 | "f078e332": "MeshKO6DCM", 325 | "43562f9c": "HWP purple", 326 | "da63aa24": "SacValleyMesh.com Router-EDH", 327 | "7c5ac998": "Ben Car", 328 | "7efef3a0": "K6HX f3a0", 329 | "c3a76c8": "Meshtastic 76c8", 330 | "e0f79f10": "Meshtastic 9f10", 331 | "1fa04c38": "jtmb-mobile-1", 332 | "6141eb92": "Meshtastic eb92", 333 | "da56af20": "Meshtastic af20", 334 | "435607a8": "Meshtastic 07a8", 335 | "501132f2": "MeshNet (MN04)", 336 | "9c4a758a": "Meshtastic 758a", 337 | "a595e686": "BB02 Alpha 🇺🇸", 338 | "bff844f3": "Ko6cnt T Echo", 339 | "886b1124": "SR Mesh SRW5", 340 | "7c5ad8f8": "Meshtastic d8f8", 341 | "fdf4a67c": "Meshtastic a67c", 342 | "3364667c": "GMesh-Base", 343 | "75b2c691": "I am Node", 344 | "36d6ef61": "Elk Grove South 1 - AD6DM", 345 | "1c5f5b6c": "AB0OO Actual", 346 | "a9361fe5": "BLUFFS ECHO 2", 347 | "3c228feb": "JB Actual", 348 | "da546ec0": "W1FRD ", 349 | "2d8c2d55": "SMRN-C-Heltec1-Folsom-Bjarne", 350 | "59e8beda": "jjw techo", 351 | "7c5b30ec": "K3M 05", 352 | "4358a5bc": "Meshtastic a5bc", 353 | "e33bcb90": "RU224 Unit One", 354 | "435639f0": "Landing1", 355 | "79b5fa1c": "Meekland Solar 🌞", 356 | "7c5a3740": "Jeff", 357 | "1fa05034": "KJ6GVE base station", 358 | "336a194c": "Capt Amesha", 359 | "c284798": "watch", 360 | "d2735fca": "Pablo Mobile", 361 | "6c9b7fed": "Jim-SVL", 362 | "e33bc9fc": "MeshNet (MN08)", 363 | "33643fb8": "RPN3 Mobile", 364 | "3fdce309": "echo one", 365 | "9c70922c": "davs2rt 922c", 366 | "336a1a18": "catalina", 367 | "7ceac537": "echo two", 368 | "32f1491f": "chimchiminy", 369 | "33696d9c": "Fiddle Leaf", 370 | "1a3947c0": "davs2rt 47c0", 371 | "da5435e0": "saahbs, email: s@ahbs.me, KN6FQM", 372 | "37bf5084": "sid2", 373 | "dd804c4e": "mesh-cyberdeck", 374 | "abdddf38": "geeksville", 375 | "336444d0": "Mesh Bear 44d0", 376 | "94de2208": "COOP", 377 | "3b46b95c": "Oakland Hills Router https://bayme.sh", 378 | "3b46a3ec": "Meshtastic a3ec", 379 | "da5c7c34": "Meshtastic 7c34", 380 | "435613ec": "R10L", 381 | "556b973d": "MeshMeOutside2", 382 | "3b46aed0": "G Base Station", 383 | "4358afa8": "BkNeko-03", 384 | "8d7b4615": "IB_TEcho", 385 | "4660ba94": "SierraLD1", 386 | "3368e8a0": "Diablo Router Backup W6CX MDARC.org", 387 | "336907bc": "Vaca Router", 388 | "da633700": "Meshtastic 3700", 389 | "777ea276": "Meshtastic a276", 390 | "242b6302": "Meshtastic 6302", 391 | "de7001a9": "Camille01a9", 392 | "5bb67c16": "Meshtastic 7c16", 393 | "33677aa0": "Airport 1 7aa0", 394 | "3b46b200": "Meshtastic b200", 395 | "da585590": "MDARC Field Day W6CX", 396 | "5879320b": "Larry 1", 397 | "15b76ebe": "larrytastic", 398 | "3006a176": "LARRY2", 399 | "336657b8": "GM-MOBILE", 400 | "38ca51af": "MeshNet (MN06) - t.ly/meshnet", 401 | "f724a310": "GBAlpha", 402 | "a1da11c5": "Meshtastic 11c5", 403 | "3b46a11c": "Zoo G2 a11c", 404 | "da5edc54": "Wp", 405 | "ea786f51": "Nonnie 62", 406 | "2f93e839": "Meshtastic e839", 407 | "f7189c74": "KG6MDW-4", 408 | "7c5ac414": "BlueBlob", 409 | "c6e038f2": "Petaluma-11-38f2", 410 | "1845dd9c": "Ko6cnt T Deck", 411 | "541e0be": "Petaluma-14-e0be", 412 | "6d00f72c": "Wilcox Radio Club 2 Mobile", 413 | "da53f46c": "Pet-Repeater-02-f46c", 414 | "deafe061": "A1", 415 | "f71e3284": "Petaluma-16-3284", 416 | "7542f38f": "Meshtastic f38f", 417 | "55c80438": "Petaluma-12-0438", 418 | "c18aa74": "ShakataGaNai/G2", 419 | "d4a8261a": "Sean Station", 420 | "e2e38580": "Rick Black 8580", 421 | "4358bcb4": "echo three https://bayme.sh", 422 | "da5763e4": "winecountry", 423 | "1fa07210": "nobody", 424 | "501ca5b6": "KW6E Main Node", 425 | "6df1ef1a": "ZP03", 426 | "e9161fe7": "DELTA-2", 427 | "e0f79b1c": "Jewel", 428 | "1ede6ce7": "Wes TRK", 429 | "68b419fb": "SAR41_19fb", 430 | "b58149bf": "Ko6cnt Mobile 🚗 ", 431 | "55c80aec": "Petaluma-13-0aec", 432 | "bd8a1078": "🌮 Node - centralvalleymesh.net", 433 | "fa6dc058": "Faer 1 Handheld", 434 | "335d8630": "KevinE Actual 2", 435 | "8235d219": "KW6E-2", 436 | "db0df5a4": "KW6E-1", 437 | "4a2f5970": "KW6E-3", 438 | "effebad9": "Meshtastic Ducky 1", 439 | "33677c7c": "Meshtastic 7c7c", 440 | "3369f15c": "ESP3 (Gilroy)", 441 | "f683bbd7": "Petaluma-15-bbd7", 442 | "f2b8c57": "JJE0 8c57", 443 | "7ab84f54": "Hulk sMesh", 444 | "f31d3d2f": "Magmadman", 445 | "da657050": "Meshtastic 7050", 446 | "da63eabc": "Meshtastic supreme", 447 | "a3d50f66": "Meshtastic_0f66", 448 | "125a1f92": "Meshtastic 1f92", 449 | "3b46a410": " - - . Known As ToWeR Bayme.sh", 450 | "da635668": "Short Omni", 451 | "f5eb77f9": "Pete Xiao 77f9 Mobile", 452 | "880ec680": "JeepMesh Turlock 2", 453 | "daee2dfa": "Eric J Solar Router K6ODS", 454 | "435605a0": "El Dorado Hills BBS (KevinE)", 455 | "e0f75be4": "JeepSmash Mobile", 456 | "aca02bea": "House of Chow Tower", 457 | "4355fd28": "KM6ZTH BB", 458 | "1fa044d8": "SMRN-RC-TBEAM-SS sacvalleymesh.com", 459 | "1fa04530": "JeepMesh Mobile 1", 460 | "7e56f6ea": "AD6DM-7", 461 | "858a0604": "JJE1_0604", 462 | "d3d3d5bc": "Bryan T-echo", 463 | "da56dea4": "Temp32", 464 | "52e3eb02": "CONO 0", 465 | "da574734": "Johnny 5 Is Alive ", 466 | "335d8768": "Meshtastic 8768", 467 | "d6e6722c": "Station 35", 468 | "675a6000": "Meshtastic 6000", 469 | "3b46b980": "KM6PNB", 470 | "af6d064c": "Curling Club 🥌 064c", 471 | "25978e80": "K6HX ROUTER", 472 | "7eff0124": "K6HX 0124", 473 | "da546f3c": "N6OIM/1-Office-6f3c", 474 | "f96a9518": "Luppy-01", 475 | "97558f1c": "RakMeAmadeus", 476 | "a0217b67": "sid3", 477 | "54383b0f": "666", 478 | "e2e5ffc8": "Meshtastic ffc8", 479 | "16699a55": "CJS mobile", 480 | "c710e99c": "UnderMesha", 481 | "46f587cb": "RPN BellTower", 482 | "c18fe8c": "NineTails", 483 | "c3a9270": "Jandros TDeck", 484 | "7c5abc54": "N6OIM Solar repeater deployment 1", 485 | "da5d6f4c": "Rebel Link 2", 486 | "cde47fc5": "Rebel Base", 487 | "da621b50": "Sierra 1b50", 488 | "da54e15c": "🌮 Mobile", 489 | "a4ee1b7b": "BLUFFS WIS 2", 490 | "da564aa8": "Pet-Repeater-01-4aa8", 491 | "da577870": "Colorado 7870", 492 | "e97d9f61": "Bullion Test Router 1.0", 493 | "efb87a8": "KTSN Tail 2", 494 | "43563a30": "3CC_Oak_RPT", 495 | "c9a523a": "Mike Romeo Tango", 496 | "da64ad1c": "Hidden Lakes Tracy Router", 497 | "3b46b99c": "ANNA", 498 | "da5476dc": "Meshtastic Ducky 3", 499 | "8bb38a8": "gramsci", 500 | "3a8bf1dc": "Packet Yeeter f1dc", 501 | "81b8669": "Packet Yeeter 8669", 502 | "f71e27dc": "Meshtastic 27dc", 503 | "ad61aa7b": "armooo", 504 | "da656f74": "emc² 📱📡", 505 | "4c9afd28": "Packet Yeeter fd28", 506 | "335c2b28": "C🍪🍪KIE 👹ONSTER", 507 | "3b469448": "RIO-1-0", 508 | "e0d01d3c": "Meshtastic 1d3c", 509 | "7c5cb128": "MeshMeOutside", 510 | "da574764": "RIO 1-3", 511 | "6b71b283": "KD2SGR T-Echo", 512 | "396b37fe": "Corte Madera Casita 🏠", 513 | "b2248b08": "RIO 1-2", 514 | "331e9c9": "B Man Mobile", 515 | "da574b54": "PBDB 4b54", 516 | "da659714": "KTSN - Cyberdeck", 517 | "abcd4f06": "MQTTastic", 518 | "5d30d1aa": "Kavala Ranch Router", 519 | "75e9a9e8": "Fair Oaks East OIM/R", 520 | "39a4cf0a": "Jellyfish", 521 | "6e22893a": "Meshtastic 893a", 522 | "19d8b9f9": "rakdaddy", 523 | "2f669ef4": "RAK Big Geek ", 524 | "1aad8e98": "Void", 525 | "336a30b0": "Colin's RV Router 1", 526 | "3368dbb0": "Glittersalad", 527 | "83714e2d": "Meshtastic Rasberry Pico", 528 | "945bfc7f": "MB Portable 1", 529 | "1c081010": "daviesgeek - centralvalleymesh.net", 530 | "e2e2e454": "Meshtastic e454", 531 | "6d70b959": "Meshtastic b959", 532 | "3369e284": "Steve Base Lincoln", 533 | "336ab50d": "Bayside", 534 | "f59deb60": "B-Dawg", 535 | "f7249b08": "GBBeta", 536 | "938bf0e0": "Proton", 537 | "45571d56": "Meshtastic 1d56", 538 | "b5beabea": "666-Car", 539 | "75d00d50": "𐕣HELL𐕣", 540 | "75cecee4": "Satans Onion", 541 | "da635a08": "Eric J Gateway", 542 | "da63f200": "PI01", 543 | "9e3a2b76": "Halo", 544 | "33666acc": "solvefunction", 545 | "1376e1b3": "Merlin Mesh - Radar [Remote Router]", 546 | "3b46a41c": "Fennec Station", 547 | "1fa06490": "yagi one", 548 | "bbb8a1f8": "AF6HO-1", 549 | "787877b7": "GCO Wis 69", 550 | "3366609c": "daviesgeek mobile - cvme.sh", 551 | "da634f34": "John Two", 552 | "da6570e8": "rd 70e8", 553 | "1edd024d": "JeepMesh Turlock Repeater 1", 554 | "ee4f5f7a": "Petaluma-17-5f7a", 555 | "c6bc4e58": "kasha1", 556 | "b0631f8d": "Meshtastic 1f8d", 557 | "43596a3c": "NorthNatomasR&D", 558 | "c1a6611d": "McMeshin718", 559 | "da634ce8": "John B", 560 | "43201df2": "MeshNet (1df2)", 561 | "e7551fc0": "JJE2 1fc0", 562 | "da574dc8": "PBDB GHOST 4dc8", 563 | "fa664110": "BlueMoon", 564 | "4355fb34": "Yeah-Yeah (Mobile)", 565 | "da657104": "N6SPP-pittsburg", 566 | "e323fd2f": "dingless", 567 | "da5ad768": "AD1M Base Station", 568 | "e2e5ff34": "Meshtastic ff34", 569 | "1fa07350": "AD1M Mobile", 570 | "3b46b8b0": "dingus lingus bringus", 571 | "fc9f699e": "Meshtastic 699e", 572 | "1fa04f00": "RtrSB 4f00", 573 | "1fa05308": "jnetb-5308", 574 | "7c5aafc0": "Green Tree Frog", 575 | "ee187530": "Meshtastic 7530", 576 | "da634310": "Meshtastic 4310", 577 | "1c66b5d0": "Sutcliff", 578 | "1b9198e4": "KK6DAC-TEcho-06", 579 | "9c387b2a": "KK6DAC-RAK", 580 | "59777a43": "HT1 🚒", 581 | "da63ef80": "Ben Green Supreme", 582 | "da5ed114": "Everybody Wants to Rule the Mesh", 583 | "13c0fe8d": "Meshtastic fe8d", 584 | "fa6c65ec": "Ben Tracker Blue", 585 | "539c7d7c": "TE 7d7c", 586 | "26221f27": "KriptoRTR", 587 | "8487f0d0": "Hello, is it mesh you're looking for?", 588 | "ba87cd39": "Meshtastic cd39", 589 | "15ca1621": "Mobile Red HT", 590 | "859468ea": "ShakataGaNai/TE", 591 | "ad3a9c0d": "Seven of Nine", 592 | "e2e39470": "Meshtastic 9470", 593 | "43587b04": "Sharptooth", 594 | "f47a5f46": "Meshtastic 5f46", 595 | "c43b6cee": "Meshtastic 6cee", 596 | "7dcfd554": "SLP d554 Base", 597 | "c39da20": "K6TRF zoid", 598 | "dfc6f464": "kasha2", 599 | "da63b588": "Yam1", 600 | "2055fe2d": "AF6HO-2", 601 | "71a66219": "sk8tastic", 602 | "e2e271d0": "Meshtastic 71d0", 603 | "7169be6f": "Center House", 604 | "a6514735": "TR-Donut", 605 | "2c7e63d": "Meshtastic e63d", 606 | "da57478c": "TATOOINE", 607 | "aedb7b91": "Fancy", 608 | "0a1fb268": "solar b268", 609 | "08ad1c4c": "KN6WPU-1", 610 | "0c284798": "watch", 611 | "0efb87a8": "KTSN Tail 2", 612 | "0365e4af": "SC base 1", 613 | "0f2b8c57": "JJE0 8c57", 614 | "0a0f494e": "Black Cat Base", 615 | "0541e0be": "Petaluma-14-e0be", 616 | "029597e8": "ElectroBoy", 617 | "0971772c": "KI6SSI Router Client", 618 | "0e0d2424": "Local Nano", 619 | "da5add08": "AD1M Base Station", 620 | "e0f74bbc": "echo four", 621 | "33664bcc": "jnetb-4bcc", 622 | "e0781356": "MeshNet - MN01", 623 | "da63f1ac": "Meshtastic f1ac", 624 | "7c4d8e5d": "Cheese", 625 | "0331e9c9": "B Man Mobile", 626 | "84887ed8": "AD6DM-1 Gateway", 627 | "0c177aec": "t-deck", 628 | "0c1a8d64": "t-deck two", 629 | "e0d0326c": "Santa Rosa Mesh", 630 | "a20afddc": "Packet Cannon", 631 | "e15015ba": "MARV", 632 | "fa4387d0": "AbelDog", 633 | "75e97dc4": "Von Kram Compound", 634 | "a78c1a2d": "Vineyard Test Router - 02", 635 | "3369ec44": "Natomas Rover", 636 | "336889b4": "Meshtastic 89b4", 637 | "97363ccf": "Meshtastic 3ccf", 638 | "da65a904": "Meshtastic a904", 639 | "538750a3": "Meshtastic 50a3", 640 | "c716f62b": "MeshNet - MN02", 641 | "31875bcb": "Andrew", 642 | "43562eb0": "HS1 MA", 643 | "7160333d": "tmp", 644 | "9a40c1b4": "Meshtastic BV4", 645 | "7c5a3600": "jnetb-3600", 646 | "042a1141": "kasha3", 647 | "5dbe37aa": "West Dublin", 648 | "fce463be": "EDH BBS (DM w/ “hi” to start)", 649 | "1c5079a4": "Paetau", 650 | "8a5ff38c": "WARAK-3B", 651 | "1e7d1bd3": "biketastic", 652 | "df743bbf": "W6KRK-2", 653 | "1c507b04": "Rudkin", 654 | "4faf6c98": "WARAK-4B", 655 | "0a15fc0c": "MaxN Static", 656 | "da5447ac": "Meshtastic 47ac", 657 | "7ed23aa8": "R3 BaseCamp 3aa8", 658 | "a20afe2c": "Oakland Hills Router [bayme.sh]", 659 | "7c5b06b0": "PJB Fr 06b0", 660 | "2505bd38": "Meshtastic bd38", 661 | "da5b96e0": "Castor", 662 | "fa6ba8dc": "HMR", 663 | "336031d8": "Supreme Beaming", 664 | "3fc69038": "SubNet-Mob2", 665 | "1c6f1edd": "Meshtastic 1edd", 666 | "cef0613a": "SubNet-Sol2", 667 | "f59ed475": "SigmacomT1", 668 | "b6d46139": "SubNet-Sol1", 669 | "e33bc5d0": "KI6ZIF", 670 | "e0d01534": "Ghoul-1-San Jose,CA", 671 | "da6578b4": "Weasel", 672 | "94a0378e": "KrrMa", 673 | "e1bc74b7": "PI05", 674 | "4829a01f": "T-ECHO1", 675 | "112c9588": "Alum Rock Mobile Node", 676 | "180f6c0b": "AF6HO-9", 677 | "1c496498": "Twirlip of the Mists", 678 | "0720a5df": "Meshtastic a5df", 679 | "51de3955": "Berkeley Home Base", 680 | "78e5449a": "Alabama 23 Solar", 681 | "e0f71b68": "echo five", 682 | "a05574b9": "Uncanny Echo", 683 | "6390989d": "Alum Rock Bike Rider", 684 | "a038a8a2": "Alum Rock Park Node", 685 | "f1e8e379": "Alum Rock Park Hiker", 686 | "8a01cd16": "HanKrr", 687 | "e2e5ff68": "Meshtastic ff68", 688 | "6dd818da": "Alum Rock Park Guest", 689 | "af88208e": "TCS Alone", 690 | "7a6c6a78": "RMFD", 691 | "06e2718a": "wismeshy", 692 | "e0cea21c": "Enginerd2", 693 | "081b8669": "Packet Yeeter 8669", 694 | "433f1d04": "Meshtastic 1d04", 695 | "15520e35": "Diablo Backup Router LF W6CX mdarc.org", 696 | "9618895e": "MTZ-01-Meowtastic_895e", 697 | "70fc6238": "Meshtastic 6238", 698 | "3973fbc9": "Alum Rock Park Long Range", 699 | "a20a0e30": "Woof Station", 700 | "da5affdc": "PE1PUY-P122", 701 | "da5acfc8": "AF6HO-3", 702 | "a0a6ef60": "Alum Rock Park", 703 | "7ab8cfd4": "PE1PUY-P121", 704 | "a20afdec": "Fennec ES", 705 | "da56ea88": "PJBMesh ea88", 706 | "0cdeac49": "N6OIM e-Bike", 707 | "061e7f80": "username 1", 708 | "6887c85c": "KD Solar", 709 | "14b419bc": "CCC-01-Meowtastic_19bc", 710 | "767a5cde": "Meshtastic 5cde", 711 | "0c39dc38": "Deck38", 712 | "289e7980": "Rick Black 7980", 713 | "da5ad0d8": "AF6HO-LOGGER", 714 | "da63df9c": "MDUB", 715 | "06b68eaa": "Fair Oaks East N6OIM-R", 716 | "33697adc": "AI6KG-M3 @ AI6KG QTH (ai6kg@arrl.net)", 717 | "c0c3330c": "AD1M Mobile", 718 | "de4b5f0e": "white@", 719 | "da5ad43c": "Meshtastic d43c", 720 | "473c78bd": "ShakataGaNai/SolarExp", 721 | "1c496f64": "CCCKM 6f64", 722 | "3a3671bd": "BleKrr", 723 | "a20a105c": "Base G2 Berryessa", 724 | "013935bf": "ADSB-01", 725 | "e8f2fee9": "Proto", 726 | "a20a1264": "KN6YPJ", 727 | "7a6c6764": "Three Emus", 728 | "33687fcc": "Gray WT", 729 | "336462e4": "Meshtastic 62e4", 730 | "43588a50": "CP_TOWER_1", 731 | "0acffb06": "Meshtastic fb06", 732 | "73ad83a8": "Mosh WisMesh", 733 | "433abb80": "Meshtastic bb80", 734 | "7eff2e1c": "KI6TSF-1 base", 735 | "8a8130ec": "Yorkville Solar", 736 | "433efcb4": "Meshtastic fcb4", 737 | "da6221f4": "Woodbridge router", 738 | "a86802a8": "Ripon_Router", 739 | "1c576671": "Ybld12", 740 | "da577260": "KJ6DLF-6", 741 | "da6389d0": "KJ6DLF-3", 742 | "4358f894": "Paranoid Android", 743 | "e1c3ea30": "Waldo", 744 | "2ca1dae3": "Ko6cnt Base https://bayme.sh", 745 | "d9cd249b": "BleKrr", 746 | "471e2322": "VokeMesh Solar", 747 | "5dbb1588": "KK6DAC-t1000e-51", 748 | "0cc3ec2f": "KK6DAC-TEcho02", 749 | "58f3319c": "KK6DAC-ATV-48", 750 | "9783e8cd": "PI Headquarters", 751 | "4358a7d8": "BBS Server a7d8", 752 | "e73010bf": "NH7G-Echo", 753 | "1174481e": "Cheese", 754 | "55673f6e": "Meshtastic 3f6e", 755 | "a20a1878": "JG2B", 756 | "433ee628": " Montebello Base", 757 | "75dc0984": "HMR", 758 | "46eb2e39": "Router-01", 759 | "abb1bc86": "Meshtastic bc86", 760 | "2afb6621": "Uncanny Bunny", 761 | "95be86b4": "Jjh", 762 | "96ebca5c": "Alabama 23 Base", 763 | "5eb9ee68": "Hel", 764 | "54357e21": "Burrito Supreme ", 765 | "4358d288": "Vogon", 766 | "a20a0e14": "PE1PUY-P123", 767 | "7615d942": "biglargeclarke.com", 768 | "43596ebc": "Deep Thought", 769 | "da5755e4": "KJ6DLF-2", 770 | "eb2b9e77": "Uncanny Roof", 771 | "65e3080d": "Base@MeshWeasel.com", 772 | "1ecf2e19": "Zender/Solar", 773 | "336989f4": "J²Red", 774 | "5d829b19": "PI STS", 775 | "43578bd8": "Meshtastic 8bd8", 776 | "4359d77c": "Alabama 23 HT", 777 | "a20a189c": "Meshtastic 189c", 778 | "62539b0c": "RuggedRouter2c26", 779 | "bd74f8c0": "J²White", 780 | "9657dbfb": "BASTRD1", 781 | "335d8468": "Gray 256", 782 | "da56c720": "Cupertino Hogwarts", 783 | "12ca12b0": "Foo", 784 | "ca313f23": "AI6KG-M0", 785 | "eb09b162": "KK6DAC-sx1262-41", 786 | "4ffe85cb": "Fox Yomp", 787 | "30256cab": "Lemonade", 788 | "3fa6c0b4": "Meshtastic 6STOSC", 789 | "066999ca": "TwoRock-Mobile", 790 | "1c496f28": "TBB4f28", 791 | "7a6bf250": "SR Av Repeater (no DM)", 792 | "f60b9160": "Meshtastic 9160", 793 | "4359680c": "ObliteRon-2", 794 | "2a427a18": "matsulabs.etsy.com", 795 | "b5fedcd3": "SM-Node-Bayside", 796 | "a2ebd95c": "Three Emus g2", 797 | "6e4e5008": "DAVS2RT 5008", 798 | "cb3b58cf": "Air Monitor", 799 | "e2e192b4": "floathouse-BBS", 800 | "baf51ee2": "T-1000e 1ee2", 801 | "eb976cd5": "NH7G-SJC", 802 | "384a22ab": "Tekify Fiber & Wireless", 803 | "aff52385": "SubNet-Trk3", 804 | "f0af12fc": "Fennec", 805 | "4357f8b8": "dpup actual", 806 | "1ec1810d": "RakOfAges", 807 | "0e175d01": "CM Router 2", 808 | "22a182a8": "Yorkville Ridgetop", 809 | "88f6bc4a": "Platinum Toro RF Deathstar mrymesh.net", 810 | "e2e2dd10": "Lofty Sky", 811 | "433f00e4": "Red Dwarf 10", 812 | "e2e5bd2c": "Base Station bd2c", 813 | "433f0700": "Meshtastic 0700", 814 | "1fa04a10": "DeepMosaic", 815 | "1c4966e4": "DocB Roof T-Beam", 816 | "4ac8dd86": "Packet Eater dd86", 817 | "1c4980a8": "Salvatore Cordileone's node", 818 | "114e2dbd": "Silver Fox T-Echo", 819 | "435956d4": "Meshtastic 56d4", 820 | "a20a1808": "Yorkville Base", 821 | "303275a0": "Meshtastic 75a0", 822 | "aaf05298": "Packet Eater 5298", 823 | "ca43b632": "kelsey", 824 | "7eb78991": "Mt. Diablo West", 825 | "dd62cbee": "AI6KG - bambam (waveshare on rpi)", 826 | "4ddadd9c": "N6IJ Radio Club mrymesh.net", 827 | "433a5f90": "DocB Desktop Heltec", 828 | "3364411c": "Meshtastic 411c", 829 | "66c8d1ad": "Lofi", 830 | "335c9dac": "esc Router", 831 | "da63e16c": "Bradford Farm Router", 832 | "3b6517c7": "KK6DAC-RAK-39", 833 | "433b9c3c": "DocB Heltec ", 834 | "ebc017d5": "KK6DAC-RAK-Mobile", 835 | "bd4faaa0": "Burger", 836 | "57c918dc": "CHEKOV mrymesh.net", 837 | "391e55d8": "tsl3", 838 | "1c497dec": "Eagle1" 839 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meshtastic-messenger", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@redis/json": "^1.0.6", 14 | "@sentry/node": "^8.10.0", 15 | "@sentry/profiling-node": "^8.10.0", 16 | "@types/node": "^18.0.6", 17 | "axios": "^1.7.2", 18 | "caprover": "^2.3.0", 19 | "mqtt": "^5.7.2", 20 | "postgres": "^3.4.4", 21 | "protobufjs": "^7.3.2", 22 | "redis": "^4.6.14", 23 | "tsx": "^4.7.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /replit.nix: -------------------------------------------------------------------------------- 1 | {pkgs}: { 2 | deps = [ 3 | pkgs.redis 4 | pkgs.dig 5 | ]; 6 | } 7 | -------------------------------------------------------------------------------- /src/FifoKeyCache.ts: -------------------------------------------------------------------------------- 1 | class FifoKeyCache { 2 | maxSize: number; 3 | currentIndex: number; 4 | cache: string[]; 5 | 6 | constructor(maxSize = 500) { 7 | this.maxSize = maxSize; 8 | this.currentIndex = 0; 9 | this.cache = []; 10 | } 11 | 12 | exists(key: string): boolean { 13 | return this.cache.includes(key); 14 | } 15 | 16 | add(key: string): boolean { 17 | const isLastSlot = this.currentIndex === this.maxSize - 1; 18 | this.cache[this.currentIndex] = key; 19 | this.currentIndex = (this.currentIndex + 1) % this.maxSize; 20 | return isLastSlot; 21 | } 22 | 23 | debuger() { 24 | console.log(this.cache); 25 | } 26 | } 27 | 28 | export default FifoKeyCache; 29 | -------------------------------------------------------------------------------- /src/MeshPacketQueue.ts: -------------------------------------------------------------------------------- 1 | /* 2 | MeshPacket { 3 | from: 2714925544, 4 | to: 2701269827, 5 | channel: 8, 6 | encrypted: , 7 | id: 2453211154, 8 | rxTime: 1716065361, 9 | rxSnr: 10.25, 10 | hopLimit: 1, 11 | wantAck: true, 12 | rxRssi: -79, 13 | hopStart: 3, 14 | decoded: Data { 15 | portnum: 1, 16 | payload: 17 | } 18 | } 19 | */ 20 | 21 | export interface Data { 22 | portnum: number; 23 | payload: Buffer; 24 | } 25 | 26 | export interface MeshPacket { 27 | from: number; 28 | to: number; 29 | channel: number; 30 | encrypted: Buffer; 31 | id: number; 32 | rxTime: number; 33 | rxSnr: number; 34 | hopLimit: number; 35 | wantAck: boolean; 36 | rxRssi: number; 37 | hopStart: number; 38 | decoded: Data; 39 | } 40 | 41 | export interface ServiceEnvelope { 42 | packet: MeshPacket; 43 | mqttTime: Date; 44 | channelId: string; 45 | gatewayId: string; 46 | topic: string; 47 | mqttServer: string; 48 | } 49 | 50 | export interface PacketGroup { 51 | id: number; 52 | time: Date; 53 | rxTime: number; 54 | serviceEnvelopes: ServiceEnvelope[]; 55 | } 56 | 57 | class MeshPacketQueue { 58 | queue: PacketGroup[]; 59 | 60 | constructor() { 61 | this.queue = []; 62 | } 63 | 64 | exists(packetId: number): boolean { 65 | return this.queue.some((packetGroup) => packetGroup.id === packetId); 66 | } 67 | 68 | getIndex(packetId: number): number { 69 | return this.queue.findIndex((packetGroup) => packetGroup.id === packetId); 70 | } 71 | 72 | add(serviceEnvelope: ServiceEnvelope, topic: string, mqttServer: string) { 73 | serviceEnvelope.mqttTime = new Date(); 74 | serviceEnvelope.topic = topic; 75 | serviceEnvelope.mqttServer = mqttServer; 76 | const grouptIndex = this.getIndex(serviceEnvelope.packet.id); 77 | if (grouptIndex === -1) { 78 | this.queue.push({ 79 | id: serviceEnvelope.packet.id, 80 | time: serviceEnvelope.mqttTime, 81 | rxTime: serviceEnvelope.packet.rxTime, 82 | serviceEnvelopes: [serviceEnvelope], 83 | }); 84 | } else { 85 | this.queue[grouptIndex].serviceEnvelopes.push(serviceEnvelope); 86 | } 87 | } 88 | 89 | popPacketGroupsOlderThan(time: number): PacketGroup[] { 90 | const packetGroups = this.queue.filter( 91 | (packetGroup) => packetGroup.time.getTime() < time, 92 | ); 93 | this.queue = this.queue.filter( 94 | (packetGroup) => packetGroup.time.getTime() >= time, 95 | ); 96 | return packetGroups; 97 | } 98 | 99 | size() { 100 | return this.queue.length; 101 | } 102 | } 103 | 104 | export default MeshPacketQueue; 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 7 | "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | // "outDir": "./", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | "noEmit": true /* Do not emit outputs. */, 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 40 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 41 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 42 | /* Module Resolution Options */ 43 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | "typeRoots": [ 48 | "./node_modules/@types" 49 | ] /* List of folders to include type definitions from. */, 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | /* Experimental Options */ 56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 58 | /* Advanced Options */ 59 | "skipLibCheck": true /* Skip type checking of declaration files. */, 60 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 61 | }, 62 | "exclude": ["node_modules", ".build"] 63 | } 64 | --------------------------------------------------------------------------------