Compare commits

..

45 Commits

Author SHA1 Message Date
2a55bd056f more cleanup 2025-04-17 22:09:18 -04:00
702da27468 more clean up 2025-04-17 21:27:54 -04:00
8e99669487 phase out old config stuff 2025-04-17 19:16:41 -04:00
63b823a7a1 more code cleanup 2025-04-17 18:45:15 -04:00
600ed7bbb3 more dep cleanup 2025-04-16 23:18:51 -04:00
82adde997e remove unused deps 2025-04-16 23:12:38 -04:00
5ec119b6c4 dont build on branch pushes 2025-04-16 23:09:46 -04:00
629825b53d commit work on map transition 2025-04-16 23:09:19 -04:00
18df989fc6 fix markercluster issues
All checks were successful
Build Docker containers / Build (push) Successful in 40s
2025-04-16 01:49:20 -04:00
e694ffa66a remove extra )
All checks were successful
Build Docker containers / Build (push) Successful in 40s
2025-04-16 00:54:04 -04:00
da99bfdeef add arrowheads plugin 2025-04-16 00:53:54 -04:00
b2a9488efd add back text-message-embed
Some checks failed
Build Docker containers / Build (push) Failing after 10s
2025-04-16 00:50:27 -04:00
6b7149905a fix background colors in traceroute window
Some checks failed
Build Docker containers / Build (push) Failing after 10s
2025-04-16 00:43:50 -04:00
8497053aed fix missing nodes, fix issue with traceroutes
All checks were successful
Build Docker containers / Build (push) Successful in 44s
2025-04-16 00:37:24 -04:00
bfdd6cba22 fix nodes all showing offline
All checks were successful
Build Docker containers / Build (push) Successful in 42s
2025-04-16 00:30:03 -04:00
6554d270bc fix call to HardwareInfo
All checks were successful
Build Docker containers / Build (push) Successful in 39s
2025-04-16 00:28:05 -04:00
bcaa1f2c20 fix typo in neighbor-info
All checks were successful
Build Docker containers / Build (push) Successful in 49s
2025-04-16 00:23:09 -04:00
0a7e456173 disable cache when building
All checks were successful
Build Docker containers / Build (push) Successful in 42s
2025-04-16 00:17:40 -04:00
d50fe75759 use ES modules
All checks were successful
Build Docker containers / Build (push) Successful in 8s
2025-04-16 00:15:50 -04:00
d963520486 use EM modules
All checks were successful
Build Docker containers / Build (push) Successful in 21s
2025-04-16 00:13:12 -04:00
e025140ab4 switch to ES modules
All checks were successful
Build Docker containers / Build (push) Successful in 47s
2025-04-16 00:08:48 -04:00
e8095fce81 update docker-compose
All checks were successful
Build Docker containers / Build (push) Successful in 17s
2025-04-15 23:48:54 -04:00
af5690e524 rewrite parser for mqtt
All checks were successful
Build Docker containers / Build (push) Successful in 30s
2025-04-15 21:26:45 -04:00
33726de0cb start work on lora ingest app
All checks were successful
Build Docker containers / Build (push) Successful in 16s
2025-04-15 20:16:04 -04:00
7b3e6e4fd1 remove protos as we are using the js builds now
All checks were successful
Build Docker containers / Build (push) Successful in 1m13s
2025-04-15 18:08:52 -04:00
0456f8a7b3 switch to js protobufs 2025-04-15 18:07:20 -04:00
913406d1b9 add npmrc for jsr repo
All checks were successful
Build Docker containers / Build (push) Successful in 31s
2025-04-15 17:56:32 -04:00
1f1c0e9881 use @meshtastic/protobuf 2025-04-15 17:56:19 -04:00
fb055785a5 last time updating names
All checks were successful
Build Docker containers / Build (push) Successful in 22s
2025-04-15 15:57:45 -04:00
5035a35554 update image names again
All checks were successful
Build Docker containers / Build (push) Successful in 16s
2025-04-15 15:55:44 -04:00
b2f5da9c88 fix docker push commands
All checks were successful
Build Docker containers / Build (push) Successful in 33s
2025-04-15 15:54:38 -04:00
9893ed0bcc fix image names, rename map-web to just map
Some checks failed
Build Docker containers / Build (push) Failing after 54s
2025-04-15 15:53:19 -04:00
77a3a5e288 remove unused docker directory
Some checks failed
Build Docker containers / Build (push) Failing after 9s
2025-04-15 15:51:55 -04:00
e44c578195 update dockerfiles 2025-04-15 15:51:46 -04:00
911983d250 add directory for common assets 2025-04-15 15:51:27 -04:00
c0a59bd30a update docker-compose for new images 2025-04-15 15:41:40 -04:00
4173a97e51 update for new images 2025-04-15 15:39:40 -04:00
8eaf49fd54 cleanup root directory 2025-04-15 15:36:26 -04:00
7f583e73b0 remove src directory 2025-04-15 15:35:04 -04:00
f20031e2ea add mqtt listener 2025-04-15 15:34:49 -04:00
2da0ee24e4 add cli for admin tools 2025-04-15 15:33:50 -04:00
4c39cee484 update docker and git ignore 2025-04-15 15:33:30 -04:00
196063c23b add webapp, move frontend to webapp folder 2025-04-15 15:33:08 -04:00
f97b3b5185 move protos to root 2025-04-15 15:31:23 -04:00
95cf93d747 remove service files 2025-04-15 15:28:29 -04:00
315 changed files with 15125 additions and 46219 deletions

View File

@ -1,2 +1,8 @@
.env
node_modules
*/prisma
!common/prisma
*/node_modules
*/Dockerfile
*/.dockerignore
webapp/frontend/node_modules

View File

@ -1,5 +1,8 @@
name: Build Docker containers
on: [push]
on:
push:
branches:
- master
jobs:
Build:
@ -12,7 +15,14 @@ jobs:
run: docker login git.arinity.org -u matt -p ${{ secrets.DOCKER_PUSH_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v4
- name: Build docker image
run: docker build -t git.arinity.org/ctmesh/map:latest .
- name: Push image
run: docker push git.arinity.org/ctmesh/map:latest
- name: Build web app image
run: docker build --no-cache -f webapp/Dockerfile -t git.arinity.org/ctmesh/map:latest .
- name: Build mqtt listener image
run: docker build --no-cache -f mqtt/Dockerfile -t git.arinity.org/ctmesh/map-mqtt:latest .
- name: Build cli image
run: docker build --no-cache -f cli/Dockerfile -t git.arinity.org/ctmesh/map-cli:latest .
- name: Push images
run: |
docker push git.arinity.org/ctmesh/map:latest
docker push git.arinity.org/ctmesh/map-mqtt:latest
docker push git.arinity.org/ctmesh/map-cli:latest

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
.idea/
node_modules
*/prisma
*/dist
# Keep environment variables out of version control
.env

View File

@ -1,10 +0,0 @@
FROM node:lts
# add project files to /app
ADD ./ /app
WORKDIR /app
# install node dependencies
RUN npm install
EXPOSE 8080

12
cli/Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM node:lts-alpine3.17
# add project files to /app
ADD ./cli /app
ADD ./common /app
ADD ./mqtt/utils /app/utils
WORKDIR /app
# install node dependencies
RUN npm install && npx prisma generate
ENTRYPOINT ["node", "index.js"]

18
cli/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "mqtt",
"version": "1.0.0",
"main": "index.js",
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@prisma/client": "^5.11.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^7.0.1",
"mqtt": "^5.11.0",
"protobufjs": "^7.5.0"
},
"devDependencies": {
"prisma": "^5.10.2"
}
}

8
common/entrypoint.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
set -e
echo "Running migrations"
npx prisma migrate deploy
echo "Starting application"
node index.js "$@"

View File

@ -2,14 +2,13 @@ services:
# listens to mqtt packets and saves to database
meshtastic-mqtt:
container_name: meshtastic-mqtt
image: git.arinity.org/ctmesh/map:latest
image: git.arinity.org/ctmesh/map-mqtt:latest
depends_on:
database:
condition: service_healthy
command: /app/docker/mqtt.sh
command: "--mqtt-broker-url= --mqtt-topic=msh/US/# --collect-service-envelopes --collect-neighbor-info --collect-waypoints"
environment:
DATABASE_URL: "mysql://root:password@database:3306/meshtastic-map?connection_limit=100"
MQTT_OPTS: "--mqtt-broker-url=" # add any custom mqtt.js options here
# runs the web map ui
meshtastic-map:
@ -18,12 +17,11 @@ services:
depends_on:
database:
condition: service_healthy
command: /app/docker/map.sh
command: "--port=8080"
ports:
- 8080:8080/tcp
environment:
DATABASE_URL: "mysql://root:password@database:3306/meshtastic-map?connection_limit=100"
MAP_OPTS: "" # add any custom index.js options here
# runs the database to store everything from mqtt
database:

View File

@ -1,7 +0,0 @@
#!/bin/sh
echo "Running migrations"
npx prisma migrate dev
echo "Starting map ui"
exec node src/index.js ${MAP_OPTS}

View File

@ -1,7 +0,0 @@
#!/bin/sh
echo "Running migrations"
npx prisma migrate dev
echo "Starting mqtt listener"
exec node src/mqtt.js ${MQTT_OPTS}

View File

@ -1,10 +0,0 @@
# Donate
Thank you for considering donating, this helps support my work on this project 😁
## How can I donate?
- Bitcoin: bc1qy22smke8n4c54evdxmp7lpy9p0e6m9tavtlg2q
- Ethereum: 0xc64CFbA5D0BF7664158c5671F64d446395b3bF3D
- Buy me a Coffee: [https://ko-fi.com/liamcottle](https://ko-fi.com/liamcottle)
- Sponsor on GitHub: [https://github.com/sponsors/liamcottle](https://github.com/sponsors/liamcottle)

File diff suppressed because it is too large Load Diff

View File

@ -1,141 +0,0 @@
<script setup>
const emit = defineEmits(['reload', 'randomNode', 'searchClick']);
import { ref } from 'vue';
import { state, searchedNodes } from '../store.js';
import { getNodeColor, getNodeTextColor } from '../utils.js';
</script>
<template>
<div class="flex bg-white p-2 border-gray-300 border-b h-16">
<!-- close mobile search button -->
<div v-if="state.mobileSearchVisible" class="my-auto">
<a @click="state.mobileSearchVisible = false" href="javascript:void(0)" class="rounded-full">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</div>
</a>
</div>
<!-- icon -->
<div v-if="!state.mobileSearchVisible" class="hidden sm:block my-auto mr-3">
<img class="w-10 h-10 rounded" src="/images/icon.png"/>
</div>
<!-- app info -->
<div v-if="!state.mobileSearchVisible" class="my-auto leading-tight">
<div class="font-bold">CT Mesh Map</div>
<div class="text-sm">
Created by <a class="link" target="_blank" href="https://liamcottle.com">Liam Cottle</a>
</div>
</div>
<!-- search bar -->
<div class="mx-3 flex-1 relative" :class="{ 'hidden lg:block': !state.mobileSearchVisible }">
<input v-model="state.searchText" type="text" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" :placeholder="`Search ${state.nodes.length} nodes...`">
<div v-if="state.searchText !== ''" class="absolute z-search bg-white w-full border border-gray-200 rounded-lg shadow-md mt-1 overflow-y-scroll max-h-80 divide-y divide-gray-200">
<template v-if="searchedNodes.length > 0">
<div @click="$emit('searchClick', node)" class="flex space-x-2 p-2 hover:bg-gray-100 cursor-pointer" v-for="node of searchedNodes">
<div>
<div class="flex rounded-full h-12 w-12 text-white shadow" :style="{backgroundColor: getNodeColor(node.node_id)}" :class="[ `text-[${getNodeTextColor(node.node_id)}]` ]">
<div class="mx-auto my-auto drop-shadow-sm">{{ node.short_name }}</div>
</div>
</div>
<div>
<div class="text-gray-900" :class="{ 'text-red-500': node.latitude == null || node.longitude == null }">{{ node.long_name !== '' ? node.long_name : "-" }}</div>
<div class="flex space-x-1 text-sm text-gray-700">
<div>{{ node.node_id_hex }} / {{ node.node_id }}</div>
</div>
</div>
</div>
<div v-if="searchedNodes.length === 500" class="text-gray-500 text-sm px-2 py-1">
Only the first 500 results are shown.
</div>
</template>
<template v-else>
<div class="p-2">
No results found...
</div>
</template>
</div>
</div>
<!-- header action buttons -->
<div v-if="!state.mobileSearchVisible" class="flex my-auto ml-auto mr-0 sm:mr-2 space-x-1 sm:space-x-2">
<a @click="state.infoModalVisible = !state.infoModalVisible" href="javascript:void(0)" class="tooltip rounded-full">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
</div>
<div class="hidden sm:block">
<span class="tooltip-text">About</span>
</div>
</a>
<a @click="state.mobileSearchVisible = true" href="javascript:void(0)" class="tooltip rounded-full block lg:hidden">
<div id="search-button" class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"></path>
<path d="M21 21l-6 -6"></path>
</svg>
</div>
<div class="hidden sm:block">
<span class="tooltip-text">Search</span>
</div>
</a>
<a @click="state.hardwareStatsVisible = !state.hardwareStatsVisible" href="javascript:void(0)" class="tooltip rounded-full hidden sm:block">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</svg>
</div>
<div class="hidden sm:block">
<span class="tooltip-text">Devices</span>
</div>
</a>
<a href="#" class="tooltip rounded-full hidden lg:block" @click="$emit('randomNode')">
<div id="random-button" class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 4l3 3l-3 3"></path>
<path d="M18 20l3 -3l-3 -3"></path>
<path d="M3 7h3a5 5 0 0 1 5 5a5 5 0 0 0 5 5h5"></path>
<path d="M21 7h-5a4.978 4.978 0 0 0 -3 1m-4 8a4.984 4.984 0 0 1 -3 1h-3"></path>
</svg>
</div>
<div class="hidden sm:block">
<span class="tooltip-text">Random</span>
</div>
</a>
<a @click="state.settingsVisible = !state.settingsVisible" href="javascript:void(0)" class="tooltip rounded-full">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</div>
<div class="hidden sm:block">
<span class="tooltip-text">Settings</span>
</div>
</a>
<a href="#" class="tooltip rounded-full" @click="$emit('reload')">
<div id="reload-button" class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full" :class="{'animate-spin': state.loading}">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path>
<path d="M20 4v5h-5"></path>
</svg>
</div>
<div class="hidden sm:block">
<span class="tooltip-text">Reload</span>
</div>
</a>
</div>
</div>
</template>

View File

@ -1,207 +0,0 @@
<script setup>
const props = defineProps(['node']);
import axios from 'axios';
import moment from 'moment';
import { Chart, TimeScale, LinearScale, LineController, Tooltip, Legend, PointElement, LineElement } from 'chart.js';
import 'chartjs-adapter-moment';
import { onMounted, useTemplateRef, watch } from 'vue';
import { state } from '../../store.js';
import { deviceMetricsTimeRange } from '../../config.js';
import { formatUptimeSeconds, getTimeSpans } from '../../utils.js';
import { useStorage } from '@vueuse/core';
const deviceMetricsChartEl = useTemplateRef('device-metrics-chart');
function initChart() {
// destroy existing chart
const existingChart = Chart.getChart(deviceMetricsChartEl.value);
if (existingChart != null) {
existingChart.destroy();
}
// create chart data
const labels = [];
const batteryMetrics = [];
const channelUtilizationMetrics = [];
const airUtilTxMetrics = [];
for(const deviceMetric of state.selectedNodeDeviceMetrics) {
labels.push(moment(deviceMetric.created_at));
batteryMetrics.push(deviceMetric.battery_level);
channelUtilizationMetrics.push(deviceMetric.channel_utilization);
airUtilTxMetrics.push(deviceMetric.air_util_tx);
}
// create chart
new Chart(deviceMetricsChartEl.value, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Battery Level',
borderColor: '#3b82f6',
backgroundColor: '#3b82f6',
pointStyle: false, // no points
fill: false,
data: batteryMetrics,
},
{
label: 'Channel Util',
borderColor: '#22c55e',
backgroundColor: '#22c55e',
showLine: false, // no lines between points
fill: false,
data: channelUtilizationMetrics,
},
{
label: 'Air Util TX',
borderColor: '#f97316',
backgroundColor: '#f97316',
showLine: false, // no lines between points
fill: false,
data: airUtilTxMetrics,
},
],
},
options: {
responsive: true,
borderWidth: 2,
elements: {
point: {
radius: 2,
},
},
scales: {
x: {
position: 'top',
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MMM DD', // Jan 01
},
},
},
y: {
min: 0,
max: 101, // 101 is "Plugged In", need to include for tooltip to work
ticks: {
callback: (label) => `${label}%`,
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
mode: "index",
intersect: false,
callbacks: {
label: (item) => {
return `${item.dataset.label}: ${item.formattedValue}%`;
},
},
},
},
},
});
}
watch(
() => state.selectedNodeDeviceMetrics,
(newValue) => {
if (newValue !== []) {
initChart()
}
}, {deep: true}
)
onMounted(() => {
Chart.register(TimeScale, LinearScale, LineController, Tooltip, Legend, PointElement, LineElement)
})
</script>
<template>
<!-- device metrics -->
<div>
<div class="flex bg-gray-200 p-2 font-semibold">
<div class="my-auto">Device Metrics</div>
<div class="my-auto ml-auto">
<select v-model="deviceMetricsTimeRange" class="block w-full rounded-md border-0 py-0.5 pl-2 pr-8 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-500 sm:text-sm sm:leading-6">
<option v-for="(range, index) in getTimeSpans()" :value="index">{{ range.name }}</option>
</select>
</div>
</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<!-- device metrics chart -->
<li>
<div class="px-4 py-2">
<div class="w-full">
<canvas id="deviceMetricsChart" style="height:150px;" ref="device-metrics-chart"></canvas>
<div class="flex">
<div class="mx-auto flex space-x-2">
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-blue-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Battery Level</div>
</div>
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-green-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Channel Utilization</div>
</div>
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-orange-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Air Util TX</div>
</div>
</div>
</div>
</div>
</div>
</li>
<!-- battery level -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Battery Level</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.battery_level">
<span v-if="props.node.battery_level > 100">Plugged In</span>
<span v-else>{{ props.node.battery_level }}%</span>
</span>
<span v-else>???</span>
</div>
</li>
<!-- voltage -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Voltage</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.voltage">{{ Number(props.node.voltage).toFixed(2) }}V</span>
<span v-else>???</span>
</div>
</li>
<!-- channel utilization -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Channel Utilization</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.channel_utilization">{{ Number(props.node.channel_utilization).toFixed(2) }}%</span>
<span v-else>???</span>
</div>
</li>
<!-- air util tx -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Air Util Tx</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.air_util_tx">{{ Number(props.node.air_util_tx).toFixed(2) }}%</span>
<span v-else>???</span>
</div>
</li>
<!-- air util tx -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Uptime</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.uptime_seconds">{{ formatUptimeSeconds(props.node.uptime_seconds) }}</span>
<span v-else>???</span>
</div>
</li>
</ul>
</div>
</template>

View File

@ -1,202 +0,0 @@
<script setup>
const props = defineProps(['node']);
import axios from 'axios';
import moment from 'moment';
import { Chart, TimeScale, LinearScale, LineController, Tooltip, Legend, PointElement, LineElement } from 'chart.js';
import 'chartjs-adapter-moment';
import { onMounted, useTemplateRef, watch } from 'vue';
import { state } from '../../store.js';
import { environmentMetricsTimeRange } from '../../config.js';
import { formatTemperature, getTimeSpans } from '../../utils.js';
const environmentMetricsChartEl = useTemplateRef('environment-metrics-chart');
function initChart() {
// destroy existing chart
const existingChart = Chart.getChart(environmentMetricsChartEl.value);
if (existingChart != null) {
existingChart.destroy();
}
// create chart data
const labels = [];
const temperatureMetrics = [];
const relativeHumidityMetrics = [];
const barometricPressureMetrics = [];
for(const deviceMetric of state.selectedNodeEnvironmentMetrics){
labels.push(moment(deviceMetric.created_at));
temperatureMetrics.push(deviceMetric.temperature);
relativeHumidityMetrics.push(deviceMetric.relative_humidity);
barometricPressureMetrics.push(deviceMetric.barometric_pressure);
}
// create chart
new Chart(environmentMetricsChartEl.value, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Temperature',
suffix: 'ºC',
borderColor: '#3b82f6',
backgroundColor: '#3b82f6',
pointStyle: false, // no points
fill: false,
data: temperatureMetrics,
yAxisID: 'y',
},
{
label: 'Humidity',
suffix: '%',
borderColor: '#22c55e',
backgroundColor: '#22c55e',
pointStyle: false, // no points
fill: false,
data: relativeHumidityMetrics,
yAxisID: 'y',
},
{
label: 'Pressure',
suffix: 'hPa',
borderColor: '#f97316',
backgroundColor: '#f97316',
pointStyle: false, // no points
fill: false,
data: barometricPressureMetrics,
yAxisID: 'y1',
},
],
},
options: {
responsive: true,
borderWidth: 2,
spanGaps: 1000 * 60 * 60 * 3, // only show lines between metrics with a 3 hour or less gap
elements: {
point: {
radius: 2,
},
},
scales: {
x: {
position: 'top',
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MMM DD', // Jan 01
},
},
},
y: {
min: -20,
max: 100,
},
y1: {
min: 800,
max: 1100,
ticks: {
stepSize: 10,
callback: (label) => `${label} hPa`,
},
position: 'right',
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
mode: "index",
intersect: false,
callbacks: {
label: (item) => {
return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`;
},
},
},
},
}
});
}
watch(
() => state.selectedNodeEnvironmentMetrics,
(newValue) => {
if (newValue !== []) {
initChart()
}
}, {deep: true}
)
onMounted(() => {
Chart.register(TimeScale, LinearScale, LineController, Tooltip, Legend, PointElement, LineElement)
})
</script>
<template>
<div>
<div class="flex bg-gray-200 p-2 font-semibold">
<div class="my-auto">Environment Metrics</div>
<div class="my-auto ml-auto">
<select v-model="environmentMetricsTimeRange" class="block w-full rounded-md border-0 py-0.5 pl-2 pr-8 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-500 sm:text-sm sm:leading-6">
<option v-for="(range, index) in getTimeSpans()" :value="index">{{ range.name }}</option>
</select>
</div>
</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<!-- environment metrics chart -->
<li>
<div class="px-4 py-2">
<div class="w-full">
<canvas id="environmentMetricsChart" style="height:150px;" ref="environment-metrics-chart"></canvas>
<div class="flex">
<div class="mx-auto flex space-x-2">
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-blue-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Temperature</div>
</div>
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-green-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Humidity</div>
</div>
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-orange-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Pressure</div>
</div>
</div>
</div>
</div>
</div>
</li>
<!-- temperature -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Temperature</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.temperature">{{ formatTemperature(props.node.temperature) }}</span>
<span v-else>???</span>
</div>
</li>
<!-- relative humidity -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Relative Humidity</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.relative_humidity">{{ Number(props.node.relative_humidity).toFixed(0) }}%</span>
<span v-else>???</span>
</div>
</li>
<!-- barometric pressure -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Barometric Pressure</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.barometric_pressure">{{ Number(props.node.barometric_pressure).toFixed(1) }}hPa</span>
<span v-else>???</span>
</div>
</li>
</ul>
</div>
</template>

View File

@ -1,40 +0,0 @@
<script setup>
import { getRegionFrequencyRange } from '../../utils.js';
const props = defineProps(['node']);
</script>
<template>
<!-- lora config -->
<div>
<div class="bg-gray-200 p-2 font-semibold">LoRa Config</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<!-- region -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Region</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.region_name">{{ props.node.region_name }} ({{ getRegionFrequencyRange(props.node.region_name) }})</span>
<span v-else>???</span>
</div>
</li>
<!-- modem preset -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Modem Preset</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.modem_preset_name">{{ props.node.modem_preset_name }}</span>
<span v-else>???</span>
</div>
</li>
<!-- has default channel -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Has Default Channel</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.has_default_channel != null">{{ props.node.has_default_channel ? "Yes" : "No" }}</span>
<span v-else>???</span>
</div>
</li>
</ul>
</div>
</template>

View File

@ -1,41 +0,0 @@
<script setup>
import { state } from '../../store.js';
import moment from 'moment';
</script>
<template>
<div>
<div class="bg-gray-200 p-2">
<div class="font-semibold">MQTT</div>
<div class="text-sm text-gray-600">Topics this node sent packets to</div>
</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<template v-if="state.selectedNodeMqttMetrics.length > 0">
<li v-for="mqttMetric of state.selectedNodeMqttMetrics">
<div class="relative flex items-center">
<div class="block flex-1 px-4 py-2">
<div class="relative flex min-w-0 flex-1 items-center">
<div class="truncate">
<p class="truncate text-sm font-medium text-gray-900">{{ mqttMetric.mqtt_topic }}</p>
<div class="text-sm text-gray-700">Last packet {{ moment(new Date(mqttMetric.last_packet_at)).fromNow() }}</div>
</div>
</div>
</div>
</div>
</li>
</template>
<template v-else>
<li>
<div class="relative flex items-center">
<div class="block flex-1 px-4 py-2">
<div class="relative flex min-w-0 flex-1 items-center">
<div class="truncate">
<div class="text-sm text-gray-700">No packets seen on MQTT</div>
</div>
</div>
</div>
</div>
</li>
</template>
</ul>
</div>
</template>

View File

@ -1,45 +0,0 @@
<script setup>
const props = defineProps(['node']);
</script>
<template>
<div>
<div class="bg-gray-200 p-2 font-semibold">Details</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<!-- id -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">ID</div>
<div class="ml-auto text-sm text-gray-700">{{ props.node.node_id }}</div>
</li>
<!-- hex id -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Hex ID</div>
<div class="ml-auto text-sm text-gray-700">{{ props.node.node_id_hex }}</div>
</li>
<!-- role -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Role</div>
<div class="ml-auto text-sm text-gray-700">{{ props.node.role_name }}</div>
</li>
<!-- hardware -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Hardware</div>
<div class="ml-auto text-sm text-gray-700">{{ props.node.hardware_model_name }}</div>
</li>
<!-- firmware version -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Firmware</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.firmware_version">{{ props.node.firmware_version }}</span>
<span v-else>???</span>
</div>
</li>
</ul>
</div>
</template>

View File

@ -1,40 +0,0 @@
<script setup>
const props = defineProps(['node']);
import moment from 'moment';
</script>
<template>
<div>
<div class="bg-gray-200 p-2 font-semibold">Other</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<!-- first seen -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">First Seen</div>
<div class="ml-auto text-sm text-gray-700">{{ moment(new Date(props.node.created_at)).fromNow() }}</div>
</li>
<!-- last seen -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Last Seen</div>
<div class="ml-auto text-sm text-gray-700">{{ moment(new Date(props.node.updated_at)).fromNow() }}</div>
</li>
<!-- neighbours updated -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Neighbours Updated</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.neighbours_updated_at">{{ moment(new Date(props.node.neighbours_updated_at)).fromNow() }}</span>
<span v-else>???</span>
</div>
</li>
<!-- position updated -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Position Updated</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.position_updated_at">{{ moment(new Date(props.node.position_updated_at)).fromNow() }}</span>
<span v-else>???</span>
</div>
</li>
</ul>
</div>
</template>

View File

@ -1,34 +0,0 @@
<script setup>
const props = defineProps(['node']);
const emit = defineEmits(['showPositionHistory']);
</script>
<template>
<div>
<div @click.stop class="flex bg-gray-200 p-2 font-semibold">
<div class="my-auto">Position</div>
<div class="ml-auto">
<button @click="$emit('showPositionHistory', props.node.node_id)" type="button" class="rounded-sm bg-white px-2 py-1 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Show History
</button>
</div>
</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<!-- position -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Lat/Long</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.latitude && props.node.longitude">{{ props.node.latitude }}, {{ props.node.longitude }}</span>
<span v-else>???</span>
</div>
</li>
<!-- altitude -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Altitude</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="props.node.altitude">{{ props.node.altitude }}m</span>
<span v-else>???</span>
</div>
</li>
</ul>
</div>
</template>

View File

@ -1,251 +0,0 @@
<script setup>
import axios from 'axios';
import moment from 'moment';
import { Chart, TimeScale, LinearScale, LineController, Tooltip, Legend, PointElement, LineElement } from 'chart.js';
import 'chartjs-adapter-moment';
import { onMounted, useTemplateRef, watch } from 'vue';
import { state, selectedNodeLatestPowerMetric } from '../../store.js';
import { powerMetricsTimeRange } from '../../config.js';
import { getTimeSpans } from '../../utils.js';
const powerMetricsChartEl = useTemplateRef('power-metrics-chart');
function initChart() {
// destroy existing chart
const existingChart = Chart.getChart(powerMetricsChartEl.value);
if (existingChart != null) {
existingChart.destroy();
}
// create chart data
const labels = [];
const channel1VoltageReadings = [];
const channel2VoltageReadings = [];
const channel3VoltageReadings = [];
const channel1CurrentReadings = [];
const channel2CurrentReadings = [];
const channel3CurrentReadings = [];
for(const powerMetric of state.selectedNodePowerMetrics) {
labels.push(moment(powerMetric.created_at));
channel1VoltageReadings.push(powerMetric.ch1_voltage);
channel2VoltageReadings.push(powerMetric.ch2_voltage);
channel3VoltageReadings.push(powerMetric.ch3_voltage);
channel1CurrentReadings.push(powerMetric.ch1_current);
channel2CurrentReadings.push(powerMetric.ch2_current);
channel3CurrentReadings.push(powerMetric.ch3_current);
}
// create chart
new Chart(powerMetricsChartEl.value, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Ch1 Voltage',
suffix: "V",
borderColor: '#3b82f6',
backgroundColor: '#3b82f6',
pointStyle: false, // no points
fill: false,
data: channel1VoltageReadings,
yAxisID: 'y',
},
{
label: 'Ch2 Voltage',
suffix: "V",
borderColor: '#22c55e',
backgroundColor: '#22c55e',
pointStyle: false, // no points
fill: false,
data: channel2VoltageReadings,
yAxisID: 'y',
},
{
label: 'Ch3 Voltage',
suffix: "V",
borderColor: '#f97316',
backgroundColor: '#f97316',
pointStyle: false, // no points
fill: false,
data: channel3VoltageReadings,
yAxisID: 'y',
},
{
label: 'Ch1 Current',
suffix: "mA",
borderColor: '#93c5fd',
backgroundColor: '#93c5fd',
pointStyle: false, // no points
fill: false,
data: channel1CurrentReadings,
yAxisID: 'y1',
},
{
label: 'Ch2 Current',
suffix: "mA",
borderColor: '#86efac',
backgroundColor: '#86efac',
pointStyle: false, // no points
fill: false,
data: channel2CurrentReadings,
yAxisID: 'y1',
},
{
label: 'Ch3 Current',
suffix: "mA",
borderColor: '#fdba74',
backgroundColor: '#fdba74',
pointStyle: false, // no points
fill: false,
data: channel3CurrentReadings,
yAxisID: 'y1',
},
],
},
options: {
responsive: true,
borderWidth: 2,
spanGaps: 1000 * 60 * 60 * 3, // only show lines between metrics with a 3 hour or less gap
elements: {
point: {
radius: 2,
},
},
scales: {
x: {
position: 'top',
type: 'time',
time: {
unit: 'day',
displayFormats: {
day: 'MMM DD', // Jan 01
},
},
},
y: {
min: 0,
max: 30,
ticks: {
callback: (label) => `${label}V`,
},
},
y1: {
min: -500,
max: 500,
ticks: {
stepSize: 50,
callback: (label) => `${label}mA`,
},
position: 'right',
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
mode: "index",
intersect: false,
callbacks: {
label: (item) => {
return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`;
},
},
},
},
}
});
}
watch(
() => state.selectedNodePowerMetrics,
(newValue) => {
if (newValue !== []) {
initChart()
}
}, {deep: true}
)
onMounted(() => {
Chart.register(TimeScale, LinearScale, LineController, Tooltip, Legend, PointElement, LineElement)
})
</script>
<template>
<!-- power metrics -->
<div>
<div class="flex bg-gray-200 p-2 font-semibold">
<div class="my-auto">Power Metrics</div>
<div class="my-auto ml-auto">
<select v-model="powerMetricsTimeRange" class="block w-full rounded-md border-0 py-0.5 pl-2 pr-8 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-500 sm:text-sm sm:leading-6">
<option v-for="(range, index) in getTimeSpans()" :value="index">{{ range.name }}</option>
</select>
</div>
</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<!-- power metrics chart -->
<li>
<div class="px-4 py-2">
<div class="w-full">
<canvas id="powerMetricsChart" style="height:150px;" ref="power-metrics-chart"></canvas>
<div class="flex">
<div class="mx-auto flex space-x-2">
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-blue-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Channel 1</div>
</div>
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-green-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Channel 2</div>
</div>
<div class="flex mx-auto">
<div class="my-auto w-2 h-2 bg-orange-500 rounded-full"></div>
<div class="my-auto ml-1 text-sm text-gray-500">Channel 3</div>
</div>
</div>
</div>
</div>
</div>
</li>
<!-- channel 1 -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Channel 1</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="selectedNodeLatestPowerMetric">
<span v-if="selectedNodeLatestPowerMetric?.ch1_voltage">{{ Number(selectedNodeLatestPowerMetric.ch1_voltage).toFixed(2) }}V</span>
<span v-else>???</span>
<span v-if="selectedNodeLatestPowerMetric?.ch1_current"> / {{ Number(selectedNodeLatestPowerMetric.ch1_current).toFixed(2) }}mA</span>
</span>
<span v-else>???</span>
</div>
</li>
<!-- channel 2 -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Channel 2</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="selectedNodeLatestPowerMetric">
<span v-if="selectedNodeLatestPowerMetric?.ch2_voltage">{{ Number(selectedNodeLatestPowerMetric.ch2_voltage).toFixed(2) }}V</span>
<span v-else>???</span>
<span v-if="selectedNodeLatestPowerMetric?.ch2_current"> / {{ Number(selectedNodeLatestPowerMetric.ch2_current).toFixed(2) }}mA</span>
</span>
<span v-else>???</span>
</div>
</li>
<!-- channel 3 -->
<li class="flex p-3">
<div class="text-sm font-medium text-gray-900">Channel 3</div>
<div class="ml-auto text-sm text-gray-700">
<span v-if="selectedNodeLatestPowerMetric">
<span v-if="selectedNodeLatestPowerMetric?.ch3_voltage">{{ Number(selectedNodeLatestPowerMetric.ch3_voltage).toFixed(2) }}V</span>
<span v-else>???</span>
<span v-if="selectedNodeLatestPowerMetric?.ch3_current"> / {{ Number(selectedNodeLatestPowerMetric.ch3_current).toFixed(2) }}mA</span>
</span>
<span v-else>???</span>
</div>
</li>
</ul>
</div>
</template>

View File

@ -1,27 +0,0 @@
<script setup>
const props = defineProps(['node']);
import { getShareLinkForNode, copyShareLinkForNode } from '../../utils.js';
</script>
<template>
<div>
<div class="flex bg-gray-200 p-2 font-semibold">
<div class="my-auto">Share Link</div>
<div class="ml-auto">
<button @click="copyShareLinkForNode(props.node.node_id)" type="button" class="rounded-sm bg-white px-2 py-1 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
Copy
</button>
</div>
</div>
<ul role="list" class="flex-1 divide-y divide-gray-200">
<li>
<div class="relative flex items-center">
<div class="block flex-1 p-2">
<div class="flex space-x-2">
<input type="text" readonly class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" :value="getShareLinkForNode(props.node.node_id)">
</div>
</div>
</div>
</li>
</ul>
</div>
</template>

View File

@ -1,47 +0,0 @@
<script setup>
const emit = defineEmits(['dismiss']);
import { state } from '../store.js';
function dismissShowingNodeNeighbours() {
state.selectedNodeToShowNeighbours = null;
emit('dismiss');
}
</script>
<template>
<div class="relative z-sidebar" role="dialog" aria-modal="true">
<!-- sidebar -->
<transition
enter-active-class="transition duration-300 ease-in-out transform"
enter-from-class="translate-y-full"
enter-to-class="translate-y-0"
leave-active-class="transition duration-300 ease-in-out transform"
leave-from-class="translate-y-0"
leave-to-class="translate-y-full">
<div v-show="state.selectedNodeToShowNeighbours != null" class="fixed left-0 right-0 bottom-0">
<div v-if="state.selectedNodeToShowNeighbours != null" class="mx-auto w-screen max-w-md p-4">
<div class="flex h-full flex-col bg-white shadow-xl rounded-xl border">
<div class="p-2">
<div class="flex items-start justify-between">
<div>
<h2 class="font-bold">{{ state.selectedNodeToShowNeighbours.short_name }} Neighbours</h2>
<h3 v-if="state.selectedNodeToShowNeighboursType === 'weHeard'" class="text-sm">Nodes heard by {{ state.selectedNodeToShowNeighbours.short_name }}</h3>
<h3 v-if="state.selectedNodeToShowNeighboursType === 'theyHeard'" class="text-sm">Nodes that heard {{ state.selectedNodeToShowNeighbours.short_name }}</h3>
</div>
<div class="my-auto ml-3 flex h-7 items-center">
<a href="javascript:void(0)" class="rounded-full" @click="dismissShowingNodeNeighbours">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 6l-12 12"></path>
<path d="M6 6l12 12"></path>
</svg>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>

View File

@ -1,210 +0,0 @@
<script setup>
import { state } from '../store.js';
import {
nodesMaxAge,
nodesDisconnectedAge,
nodesOfflineAge,
waypointsMaxAge,
neighboursMaxDistance,
goToNodeZoomLevel,
temperatureFormat,
autoUpdatePositionInUrl,
enableMapAnimations,
} from '../config.js';
</script>
<template>
<!-- settings sidebar -->
<div class="relative z-sidebar" role="dialog" aria-modal="true">
<!-- overlay -->
<transition
enter-active-class="transition-opacity duration-300 ease-linear"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-300 ease-linear"
leave-from-class="opacity-100"
leave-to-class="opacity-0">
<div v-show="state.settingsVisible" @click="state.settingsVisible = !state.settingsVisible" class="fixed inset-0 bg-gray-900/75"></div>
</transition>
<!-- sidebar -->
<transition
enter-active-class="transition duration-300 ease-in-out transform"
enter-from-class="-translate-x-full"
enter-to-class="translate-x-0"
leave-active-class="transition duration-300 ease-in-out transform"
leave-from-class="translate-x-0"
leave-to-class="-translate-x-full">
<div v-show="state.settingsVisible" class="fixed top-0 left-0 bottom-0">
<div v-if="state.settingsVisible" class="w-screen h-full max-w-md overflow-hidden">
<div class="flex h-full flex-col bg-white shadow-xl">
<!-- slideover header -->
<div class="p-2 border-b border-gray-200 shadow-sm">
<div class="flex items-start justify-between">
<div>
<h2 class="font-bold">Settings</h2>
<h3 class="text-sm">Changes are only saved in this browser.</h3>
</div>
<div class="my-auto ml-3 flex h-7 items-center">
<a href="javascript:void(0)" class="rounded-full" @click="state.settingsVisible = false">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 6l-12 12"></path>
<path d="M6 6l12 12"></path>
</svg>
</div>
</a>
</div>
</div>
</div>
<div class="overflow-y-auto divide-y divide-gray-200">
<!-- configNodesMaxAgeInSeconds -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Nodes Max Age</label>
<div class="text-xs text-gray-600 mb-2">Nodes not updated within this time are hidden. Reload to update map.</div>
<select v-model="nodesMaxAge" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option :value="null">Show All</option>
<option value="900">15 minutes</option>
<option value="1800">30 minutes</option>
<option value="3600">1 hour</option>
<option value="10800">3 hours</option>
<option value="21600">6 hours</option>
<option value="43200">12 hours</option>
<option value="86400">24 hours</option>
<option value="172800">2 days</option>
<option value="259200">3 days</option>
<option value="345600">4 days</option>
<option value="432000">5 days</option>
<option value="518400">6 days</option>
<option value="604800">7 days</option>
</select>
</div>
<!-- configNodesDisconnectedAgeInSeconds -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Nodes Disconnected Age</label>
<div class="text-xs text-gray-600 mb-2">Nodes that have not uplinked to MQTT in this time will show as blue icons. Reload to update map.</div>
<select v-model="nodesDisconnectedAge" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option value="900">15 minutes</option>
<option value="1800">30 minutes</option>
<option value="2700">45 minutes</option>
<option value="3600">1 hour</option>
<option value="7200">2 hours</option>
<option value="10800">3 hours</option>
<option value="21600">6 hours</option>
<option value="43200">12 hours</option>
<option value="86400">24 hours</option>
<option value="172800">2 days</option>
<option value="259200">3 days</option>
<option value="345600">4 days</option>
<option value="432000">5 days</option>
<option value="518400">6 days</option>
<option value="604800">7 days</option>
</select>
</div>
<!-- configNodesOfflineAgeInSeconds -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Nodes Offline Age</label>
<div class="text-xs text-gray-600 mb-2">Nodes not updated within this time will show as red icons. Reload to update map.</div>
<select v-model="nodesOfflineAge" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option :value="null">Don't show as offline</option>
<option value="900">15 minutes</option>
<option value="1800">30 minutes</option>
<option value="2700">45 minutes</option>
<option value="3600">1 hour</option>
<option value="7200">2 hours</option>
<option value="10800">3 hours</option>
<option value="21600">6 hours</option>
<option value="43200">12 hours</option>
<option value="86400">24 hours</option>
<option value="172800">2 days</option>
<option value="259200">3 days</option>
<option value="345600">4 days</option>
<option value="432000">5 days</option>
<option value="518400">6 days</option>
<option value="604800">7 days</option>
</select>
</div>
<!-- configWaypointsMaxAgeInSeconds -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Waypoints Max Age</label>
<div class="text-xs text-gray-600 mb-2">Waypoints not updated within this time are hidden. Reload to update map.</div>
<select v-model="waypointsMaxAge" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option :value="null">Show All</option>
<option value="900">15 minutes</option>
<option value="1800">30 minutes</option>
<option value="3600">1 hour</option>
<option value="10800">3 hours</option>
<option value="21600">6 hours</option>
<option value="43200">12 hours</option>
<option value="86400">24 hours</option>
<option value="172800">2 days</option>
<option value="259200">3 days</option>
<option value="345600">4 days</option>
<option value="432000">5 days</option>
<option value="518400">6 days</option>
<option value="604800">7 days</option>
</select>
</div>
<!-- configNeighboursMaxDistanceInMeters -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Neighbours Max Distance (meters)</label>
<div class="text-xs text-gray-600 mb-2">Neighbours further than this are hidden. Reload to update map.</div>
<input type="number" v-model="neighboursMaxDistance" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
</div>
<!-- configZoomLevelGoToNode -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Zoom Level (go to node)</label>
<div class="text-xs text-gray-600 mb-2">How far to zoom map when navigating to a node.</div>
<input type="number" v-model="goToNodeZoomLevel" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
</div>
<!-- configTemperatureFormat -->
<div class="p-2">
<label class="block text-sm font-medium text-gray-900">Temperature Format</label>
<div class="text-xs text-gray-600 mb-2">Metrics will be shown in the selected format.</div>
<select v-model="temperatureFormat" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option value="celsius">Celsius (ºC)</option>
<option value="fahrenheit">Fahrenheit (ºF)</option>
</select>
</div>
<!-- configAutoUpdatePositionInUrl -->
<div class="p-2">
<div class="flex items-start">
<div class="flex items-center h-5">
<input type="checkbox" v-model="autoUpdatePositionInUrl" class="w-4 h-4 border border-gray-300 rounded-sm bg-gray-50 focus:ring-3 focus:ring-blue-300" required>
</div>
<label class="ml-2 text-sm font-medium text-gray-900">Auto Update Position in URL</label>
</div>
<div class="text-xs text-gray-600">Sets lat/lng/zoom as query parameters.</div>
</div>
<!-- configEnableMapAnimations -->
<div class="p-2">
<div class="flex items-start">
<div class="flex items-center h-5">
<input type="checkbox" v-model="enableMapAnimations" class="w-4 h-4 border border-gray-300 rounded-sm bg-gray-50 focus:ring-3 focus:ring-blue-300" required>
</div>
<label class="ml-2 text-sm font-medium text-gray-900">Enable Map Animations</label>
</div>
<div class="text-xs text-gray-600">Map will animate flying to nodes.</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>

View File

@ -1,154 +0,0 @@
<script setup>
const emit = defineEmits(['goTo']);
import moment from 'moment';
import { state } from '../store.js';
import { getNodeColor, getNodeTextColor, findNodeById, findNodeMarkerById } from '../utils.js';
</script>
<template>
<div class="relative z-sidebar" role="dialog" aria-modal="true">
<!-- overlay -->
<transition
enter-active-class="transition-opacity duration-300 ease-linear"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-300 ease-linear"
leave-from-class="opacity-100"
leave-to-class="opacity-0">
<div v-show="state.selectedTraceRoute != null" @click="state.selectedTraceRoute = null" class="fixed inset-0 bg-gray-900/75"></div>
</transition>
<!-- sidebar -->
<transition
enter-active-class="transition duration-300 ease-in-out transform"
enter-from-class="-translate-x-full"
enter-to-class="translate-x-0"
leave-active-class="transition duration-300 ease-in-out transform"
leave-from-class="translate-x-0"
leave-to-class="-translate-x-full">
<div v-show="state.selectedTraceRoute != null" class="fixed top-0 left-0 bottom-0">
<div v-if="state.selectedTraceRoute != null" class="w-screen h-full max-w-md overflow-hidden">
<div class="flex h-full flex-col bg-white shadow-xl">
<!-- slideover header -->
<div class="p-2 border-b border-gray-200 shadow-sm">
<div class="flex items-start justify-between">
<div>
<h2 class="font-bold">Traceroute #{{ state.selectedTraceRoute.id }}</h2>
<h3 class="text-sm">{{ moment(new Date(state.selectedTraceRoute.updated_at)).fromNow() }} - {{ state.selectedTraceRoute.route.length }} hops {{ state.selectedTraceRoute.channel_id ? `on ${state.selectedTraceRoute.channel_id}` : '' }}</h3>
</div>
<div class="my-auto ml-3 flex h-7 items-center">
<a href="javascript:void(0)" class="rounded-full" @click="state.selectedTraceRoute = null">
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 6l-12 12"></path>
<path d="M6 6l12 12"></path>
</svg>
</div>
</a>
</div>
</div>
</div>
<div class="overflow-y-auto">
<!-- details -->
<div class="p-2">
<ul role="list" class="space-y-3">
<!-- node that initiated traceroute -->
<li @click="$emit('goTo', state.selectedTraceRoute.to)" class="relative flex gap-x-4">
<div class="absolute left-0 top-0 flex w-12 justify-center top-3 -bottom-3">
<div class="w-px bg-gray-200"></div>
</div>
<div class="my-auto relative flex flex-none items-center justify-center">
<div>
<div class="flex rounded-full h-12 w-12 text-white shadow-sm" :class="[ `bg-[${getNodeColor(state.selectedTraceRoute.to)}]`, `text-[${getNodeTextColor(state.selectedTraceRoute.to)}]` ]">
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.to)?.short_name ?? "?" }}</div>
</div>
</div>
</div>
<div class="flex-auto py-0.5 text-sm leading-5 text-gray-500">
<div class="font-medium text-gray-900">{{ findNodeById(state.selectedTraceRoute.to)?.long_name || '???' }}</div>
<div>Hex ID: !{{ Number(state.selectedTraceRoute.to).toString(16) }}</div>
<div>Started the traceroute</div>
</div>
</li>
<!-- middleman nodes -->
<li @click="$emit('goTo', route)" v-for="route of state.selectedTraceRoute.route" class="relative flex gap-x-4">
<div class="absolute left-0 top-0 flex w-12 justify-center -bottom-3">
<div class="w-px bg-gray-200"></div>
</div>
<div class="my-auto relative flex flex-none items-center justify-center">
<div>
<div class="flex rounded-full h-12 w-12 text-white shadow" :class="[ `bg-[${getNodeColor(route)}]`, `text-[${getNodeTextColor(route)}]` ]">
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(route)?.short_name ?? "?" }}</div>
</div>
</div>
</div>
<div class="flex-auto py-0.5 text-sm leading-5 text-gray-500">
<div class="font-medium text-gray-900">{{ findNodeById(route)?.long_name || '???' }}</div>
<div>Hex ID: !{{ Number(route).toString(16) }}</div>
<div>Forwarded the packet</div>
</div>
</li>
<!-- node that replied to traceroute -->
<li @click="$emit('goTo', state.selectedTraceRoute.from)" v-if="state.selectedTraceRoute.from" class="relative flex gap-x-4">
<div class="absolute left-0 top-0 flex w-12 justify-center -bottom-3">
<div class="w-px bg-gray-200"></div>
</div>
<div class="my-auto relative flex flex-none items-center justify-center">
<div>
<div class="flex rounded-full h-12 w-12 text-white shadow" :class="[ `bg-[${getNodeColor(state.selectedTraceRoute.from)}]`, `text-[${getNodeTextColor(state.selectedTraceRoute.from)}]` ]">
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.from)?.short_name ?? "?" }}</div>
</div>
</div>
</div>
<div class="flex-auto py-0.5 text-sm leading-5 text-gray-500">
<div class="font-medium text-gray-900">{{ findNodeById(state.selectedTraceRoute.from)?.long_name || '???' }}</div>
<div>Hex ID: !{{ Number(state.selectedTraceRoute.from).toString(16) }}</div>
<div>Replied to traceroute</div>
</div>
</li>
<!-- node that gated traceroute to mqtt -->
<li @click="$emit('goTo', state.selectedTraceRoute.gateway_id)" v-if="state.selectedTraceRoute.gateway_id" class="relative flex gap-x-4">
<div class="absolute left-0 top-0 flex w-12 justify-center h-6">
<div class="w-px bg-gray-200"></div>
</div>
<div class="my-auto relative flex flex-none items-center justify-center">
<div>
<div class="flex rounded-full h-12 w-12 text-white shadow" :class="[ `bg-[${getNodeColor(state.selectedTraceRoute.gateway_id)}]`, `text-[${getNodeTextColor(state.selectedTraceRoute.gateway_id)}]` ]">
<div class="mx-auto my-auto drop-shadow-sm">{{ findNodeById(state.selectedTraceRoute.gateway_id)?.short_name ?? "?" }}</div>
</div>
</div>
</div>
<div class="flex-auto py-0.5 text-sm leading-5 text-gray-500">
<div class="font-medium text-gray-900">{{ findNodeById(state.selectedTraceRoute.gateway_id)?.long_name || '???' }}</div>
<div>Hex ID: !{{ Number(state.selectedTraceRoute.gateway_id).toString(16) }}</div>
<div>Gated the packet to MQTT</div>
</div>
</li>
</ul>
</div>
<div>
<div class="bg-gray-200 p-2 font-semibold">Raw Data</div>
<div class="text-sm text-gray-700">
<pre class="bg-gray-100 rounded-sm p-2 overflow-x-auto">{{ JSON.stringify(state.selectedTraceRoute, null, 4) }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>

View File

@ -1,29 +0,0 @@
import { useStorage } from '@vueuse/core';
// static
export const CURRENT_ANNOUNCEMENT_ID = 1;
export const BASE_PATH = '';
// string
export const temperatureFormat = useStorage('temperature-display', 'fahrenheit');
// boolean
export const autoUpdatePositionInUrl = useStorage('auto-update-url', true);
export const enableMapAnimations = useStorage('map-animations', true);
export const hasSeenInfoModal = useStorage('seen-info-modal', false);
// time in seconds
export const nodesMaxAge = useStorage('nodes-max-age', null);
export const nodesDisconnectedAge = useStorage('nodes-max-disconnected-age', 604800);
export const nodesOfflineAge = useStorage('nodes-offline-age', null);
export const waypointsMaxAge = useStorage('waypoints-max-age', 604800);
// number
export const goToNodeZoomLevel = useStorage('zoom-to-node', 15);
export const lastSeenAnnouncementId = useStorage('last-seen-announcement-id', 1);
// distance in meters
export const neighboursMaxDistance = useStorage('neighbors-distance', null);
// device info ranges
export const deviceMetricsTimeRange = useStorage('device-metrics-range', '3d');
export const powerMetricsTimeRange = useStorage('power-metrics-range', '3d');
export const environmentMetricsTimeRange = useStorage('environment-metrics-range', '3d');
// map config
export const enabledOverlayLayers = useStorage('enabled-overlay-layers', ['Legend', 'Position History']);
export const selectedTileLayerName = useStorage('selected-tile-layer', 'OpenStreetMap');

View File

@ -1,11 +0,0 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')

View File

@ -1,289 +0,0 @@
import moment from 'moment';
import L from 'leaflet/dist/leaflet.js';
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
import {
escapeString,
findNodeById,
getRegionFrequencyRange,
hasNodeUplinkedToMqttRecently,
formatPositionPrecision,
getTerrainProfileImage,
} from './utils.js';
import { state } from './store.js';
import { markRaw } from 'vue';
// state/config
let instance = null;
export function setMap(map) {
instance = markRaw(map);
}
export function getMap() {
return instance;
}
export const layerGroups = {
nodes: new L.LayerGroup(),
neighbors: new L.LayerGroup(),
waypoints: new L.LayerGroup(),
nodePositionHistory: new L.LayerGroup(),
nodeNeighbors: new L.LayerGroup(),
nodesRouter: L.markerClusterGroup({
showCoverageOnHover: false,
disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
}),
nodesClustered: L.markerClusterGroup({
showCoverageOnHover: false,
disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
}),
legend: new L.LayerGroup(),
none: new L.LayerGroup(),
};
export const tileLayers = {
"OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 22, // increase from 18 to 22
attribution: 'Tiles &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
}),
"OpenTopoMap": L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
maxZoom: 17, // open topo map doesn't have tiles closer than this
attribution: 'Tiles &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}),
"Esri Satellite": L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 21, // esri doesn't have tiles closer than this
attribution: 'Tiles &copy; <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
}),
"Google Satellite": L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
maxZoom: 21,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
attribution: 'Tiles &copy; Google | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
}),
"Google Hybrid": L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
maxZoom: 21,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
attribution: 'Tiles &copy; Google | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
}),
};
export const icons = {
mqttConnected: L.divIcon({
className: 'icon-mqtt-connected',
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
}),
mqttDisconnected: L.divIcon({
className: 'icon-mqtt-disconnected',
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
}),
offline: L.divIcon({
className: 'icon-offline',
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
}),
positionHistory: L.divIcon({
className: 'icon-position-history',
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
}),
};
// tooltips
export function getTooltipContentForWaypoint(waypoint) {
// get from node name
var fromNode = findNodeById(waypoint.from);
var tooltip = `<b>${escapeString(waypoint.name)}</b>` +
(waypoint.description ? `<br/>${escapeString(waypoint.description)}` : '') +
`<br/><br/>Expires: ${moment(new Date(waypoint.expire * 1000)).fromNow()}` +
`<br/>Lat/Lng: ${waypoint.latitude}, ${waypoint.longitude}` +
`<br/><br/>From ID: ${waypoint.from}` +
`<br/>From Hex ID: !${Number(waypoint.from).toString(16)}`;
// show node name this waypoint is from, if possible
if(fromNode != null){
tooltip += `<br/>From Node: <a href="#" onclick="goToNode(${waypoint.from})">${escapeString(fromNode.long_name) || 'Unnamed Node'}</a>`;
} else {
tooltip += `<br/>From Node: ???`;
}
// bottom info
tooltip += `<br/><br/>ID: ${waypoint.waypoint_id}`;
tooltip += `<br/>Updated: ${moment(new Date(waypoint.updated_at)).fromNow()}`;
return tooltip;
};
export function getTooltipContentForNode(node) {
// determine if node was recently heard uplinking packets to mqtt
const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node);
var mqttStatus = `<span class="text-blue-700">Disconnected</span>`;
if(node.mqtt_connection_state_updated_at){
var mqttStatusUpdatedAt = moment(new Date(node.mqtt_connection_state_updated_at)).fromNow();
if(nodeHasUplinkedToMqttRecently){
mqttStatus = `<span><span class="text-green-700">Connected</span> (${mqttStatusUpdatedAt})</span>`;
} else {
mqttStatus = `<span><span class="text-blue-700">Disconnected</span> (${mqttStatusUpdatedAt})</span>`;
}
}
var loraFrequencyRange = getRegionFrequencyRange(node.region_name);
var tooltip = `<img class="mb-4 w-40 mx-auto" src="/images/devices/${node.hardware_model_name}.png" onerror="this.classList.add('hidden')"/>` +
`<b>${escapeString(node.long_name)}</b>` +
`<br/>Short Name: ${escapeString(node.short_name)}` +
`<br/>MQTT: ${mqttStatus}` +
(node.num_online_local_nodes != null ? `<br/>Local Nodes Online: ${node.num_online_local_nodes}` : '') +
(node.position_precision != null && node.position_precision !== 32 ? `<br/>Position Precision: ${formatPositionPrecision(node.position_precision)}` : '') +
`<br/><br/>Role: ${node.role_name}` +
`<br/>Hardware: ${node.hardware_model_name}` +
(node.firmware_version != null ? `<br/>Firmware: ${node.firmware_version}` : '') +
(node.region_name != null ? `<br/>LoRa Region: ${node.region_name} (${loraFrequencyRange})` : '') +
(node.modem_preset_name != null ? `<br/>Modem Preset: ${node.modem_preset_name}` : '') +
(node.has_default_channel != null ? `<br/>Has Default Channel: ${node.has_default_channel ? "Yes" : "No"}` : '');
if(node.battery_level){
if(node.battery_level > 100){
tooltip += `<br/>Battery: ${node.battery_level > 100 ? 'Plugged In' : node.battery_level}`;
} else {
tooltip += `<br/>Battery: ${node.battery_level}%`;
}
}
if(node.voltage){
tooltip += `<br/>Voltage: ${Number(node.voltage).toFixed(2)}V`;
}
if(node.channel_utilization){
tooltip += `<br/>Ch Util: ${Number(node.channel_utilization).toFixed(2)}%`;
}
if(node.air_util_tx){
tooltip += `<br/>Air Util: ${Number(node.air_util_tx).toFixed(2)}%`;
}
// ignore alt above 42949000 due to https://github.com/meshtastic/firmware/issues/3109
if(node.altitude && node.altitude < 42949000){
tooltip += `<br/>Altitude: ${node.altitude}m`;
}
// bottom info
tooltip += `<br/><br/>ID: ${node.node_id}`;
tooltip += `<br/>Hex ID: ${node.node_id_hex}`;
tooltip += `<br/>Updated: ${moment(new Date(node.updated_at)).fromNow()}`;
tooltip += (node.neighbours_updated_at ? `<br/>Neighbours Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '');
tooltip += (node.position_updated_at ? `<br/>Position Updated: ${moment(new Date(node.position_updated_at)).fromNow()}` : '');
// show details button
tooltip += `<br/><br/><button onclick="showNodeDetails(${node.node_id});" class="border border-gray-300 bg-gray-100 p-1 w-full rounded hover:bg-gray-200 mb-1">Show Full Details</button>`;
tooltip += `<br/><button onclick="window.showNodeNeighboursThatHeardUs(${node.node_id});" class="border border-gray-300 bg-gray-100 p-1 w-full rounded hover:bg-gray-200 mb-1">Show Neighbours (Heard Us)</button>`;
tooltip += `<br/><button onclick="window.showNodeNeighboursThatWeHeard(${node.node_id});" class="border border-gray-300 bg-gray-100 p-1 w-full rounded hover:bg-gray-200">Show Neighbours (We Heard)</button>`;
tooltip += `</div>`;
return tooltip;
}
export function getNeighbourTooltipContent(type, node, neighbourNode, distanceInMeters, snr) {
// default to showing distance in meters
let distance = `${distanceInMeters} meters`;
// scale to distance in kms
if (distanceInMeters >= 1000) {
const distanceInKilometers = (distanceInMeters / 1000).toFixed(2);
distance = `${distanceInKilometers} kilometers`;
}
const terrainImageUrl = type === 'weHeard' ? getTerrainProfileImage(node, neighbourNode) : getTerrainProfileImage(neighbourNode, node);
const templates = {
'weHeard': `<b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b> heard <b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b>`
+ `<br/>SNR: ${snr}dB`
+ `<br/>Distance: ${distance}`
+ `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}`
+ `<br/>Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}`
+ (node.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '')
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`,
'theyHeard': `<b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b> heard <b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b>`
+ `<br/>SNR: ${snr}dB`
+ `<br/>Distance: ${distance}`
+ `<br/><br/>ID: ${neighbourNode.node_id} heard ${node.node_id}`
+ `<br/>Hex ID: ${neighbourNode.node_id_hex} heard ${node.node_id_hex}`
+ (neighbourNode.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(neighbourNode.neighbours_updated_at)).fromNow()}` : '')
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`,
}
return templates[type];
}
// cleanup
export function clearAllNodes() {
layerGroups.nodes.clearLayers();
layerGroups.nodesClustered.clearLayers();
layerGroups.nodesRouter.clearLayers();
};
export function clearAllNeighbors() {
layerGroups.neighbors.clearLayers();
};
export function clearAllWaypoints() {
layerGroups.waypoints.clearLayers();
};
export function clearAllPositionHistory() {
layerGroups.nodePositionHistory.clearLayers();
};
export function cleanUpPositionHistory() {
// close tooltips and popups
closeAllPopups();
closeAllTooltips();
// setup node neighbours layer
layerGroups.nodePositionHistory.clearLayers();
layerGroups.nodePositionHistory.removeFrom(getMap());
layerGroups.nodePositionHistory.addTo(getMap());
};
export function closeAllTooltips() {
getMap().eachLayer(function(layer) {
if (layer.options.pane === 'tooltipPane') {
layer.removeFrom(getMap());
}
});
};
export function closeAllPopups() {
getMap().eachLayer(function(layer) {
if (layer.options.pane === 'popupPane') {
layer.removeFrom(getMap());
}
});
};
export function cleanUpNodeNeighbors() {
// close tooltips and popups
closeAllPopups();
closeAllTooltips();
// setup node neighbours layer
layerGroups.nodeNeighbors.clearLayers();
layerGroups.nodeNeighbors.removeFrom(getMap());
layerGroups.nodeNeighbors.addTo(getMap());
};
export function clearNodeOutline() {
if (state.selectedNodeOutlineCircle) {
state.selectedNodeOutlineCircle.removeFrom(getMap());
state.selectedNodeOutlineCircle = null;
}
};
export function clearMap() {
closeAllPopups();
closeAllTooltips();
clearAllNodes();
clearAllNeighbors();
clearAllWaypoints();
clearNodeOutline();
cleanUpNodeNeighbors();
};

View File

@ -1,60 +0,0 @@
import { reactive, computed } from 'vue';
export const state = reactive({
// caches
nodes: [],
waypoints: [],
nodeMarkers: {},
// state
searchText: '',
selectedNodeOutlineCircle: null,
selectedNodeMqttMetrics: [],
selectedNodeTraceroutes: [],
selectedNodeDeviceMetrics: [],
selectedNodePowerMetrics: [],
selectedNodeEnvironmentMetrics: [],
selectedNodeToShowNeighbours: null,
selectedNodeToShowNeighbours: null,
selectedNodeToShowNeighboursType: null,
// position history
selectedNodeToShowPositionHistory: null,
positionHistoryDateTimeTo: null,
positionHistoryDateTimeFrom: null,
// ui
loading: false,
settingsVisible: false,
hardwareStatsVisible: false,
infoModalVisible: false,
mobileSearchVisible: false,
positionHistoryModalExpanded: false,
announcementVisible: false,
});
export const searchedNodes = computed(() => {
// search nodes
const nodes = state.nodes.filter((node) => {
const matchesId = node.node_id?.toLowerCase()?.includes(state.searchText.toLowerCase());
const matchesHexId = node.node_id_hex?.toLowerCase()?.includes(state.searchText.toLowerCase());
const matchesLongName = node.long_name?.toLowerCase()?.includes(state.searchText.toLowerCase());
const matchesShortName = node.short_name?.toLowerCase()?.includes(state.searchText.toLowerCase());
return matchesId || matchesHexId || matchesLongName || matchesShortName;
});
// order alphabetically by long name
nodes.sort((nodeA, nodeB) => {
const nodeALongName = nodeA.long_name || "";
const nodeBLongName = nodeB.long_name || "";
return nodeALongName.localeCompare(nodeBLongName);
});
// only return the first 500 results to avoid ui lag...
return nodes.slice(0, 500);
});
export const selectedNodeLatestPowerMetric = computed(() => {
const [ latestPowerMetric ] = state.selectedNodePowerMetrics.slice(-1);
return latestPowerMetric;
});

View File

@ -1,911 +0,0 @@
<script setup>
import Header from '../components/Header.vue';
import InfoModal from '../components/InfoModal.vue';
import HardwareModelList from '../components/HardwareModelList.vue';
import Settings from '../components/Settings.vue';
import NodeInfo from '../components/NodeInfo.vue';
import NodeNeighborsModal from '../components/NodeNeighborsModal.vue';
import NodePositionHistoryModal from '../components/NodePositionHistoryModal.vue';
import TracerouteInfo from '../components/TracerouteInfo.vue';
import Announcement from '../components/Announcement.vue';
import axios from 'axios';
import moment from 'moment';
import L from 'leaflet/dist/leaflet.js';
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue';
import { state } from '../store.js';
import {
layerGroups,
tileLayers,
icons,
getTooltipContentForWaypoint,
getTooltipContentForNode,
getNeighbourTooltipContent,
clearAllNodes,
clearAllNeighbors,
clearAllWaypoints,
clearAllPositionHistory,
cleanUpPositionHistory,
closeAllTooltips,
closeAllPopups,
cleanUpNodeNeighbors,
clearNodeOutline,
clearMap,
setMap,
getMap,
} from '../map.js';
import {
nodesMaxAge,
nodesDisconnectedAge,
nodesOfflineAge,
waypointsMaxAge,
enableMapAnimations,
goToNodeZoomLevel,
autoUpdatePositionInUrl,
neighboursMaxDistance,
enabledOverlayLayers,
selectedTileLayerName,
hasSeenInfoModal,
lastSeenAnnouncementId,
CURRENT_ANNOUNCEMENT_ID,
} from '../config.js';
import {
getColorForSnr,
getPositionPrecisionInMeters,
getTerrainProfileImage,
getRegionFrequencyRange,
escapeString,
formatPositionPrecision,
isMobile,
elementOrAnyAncestorHasClass,
findNodeById,
findNodeMarkerById,
hasNodeUplinkedToMqttRecently,
buildPath,
} from '../utils.js';
const mapEl = useTemplateRef('appMap');
// watchers
watch(
() => state.positionHistoryDateTimeTo,
(newValue) => {
if (newValue != null) {
loadNodePositionHistory(state.selectedNodeToShowPositionHistory.node_id);
}
}, {deep: true}
);
watch(
() => state.positionHistoryDateTimeFrom,
(newValue) => {
if (newValue != null) {
loadNodePositionHistory(state.selectedNodeToShowPositionHistory.node_id);
}
}, {deep: true}
);
function showNodeOutline(id) {
// remove any existing node circle
clearNodeOutline();
// find node marker by id
const nodeMarker = state.nodeMarkers[id];
if (!nodeMarker) {
return;
}
// find node by id
const node = findNodeById(id);
if (!node) {
return;
}
// add position precision circle around node
if(node.position_precision != null && node.position_precision > 0 && node.position_precision < 32){
state.selectedNodeOutlineCircle = L.circle(nodeMarker.getLatLng(), {
radius: getPositionPrecisionInMeters(node.position_precision),
}).addTo(getMap());
}
}
window.showNodeDetails = function(id) {
// find node
const node = findNodeById(id);
if (!node) {
return;
}
state.selectedNode = node;
}
window.showNodeNeighboursThatHeardUs = function(id) {
cleanUpNodeNeighbors();
// find node
const node = findNodeById(id);
if (!node) {
return;
}
// find node marker
const nodeMarker = findNodeMarkerById(node.node_id);
if (!nodeMarker) {
return;
}
// show overlay
state.selectedNodeToShowNeighbours = node;
state.selectedNodeToShowNeighboursType = 'theyHeard';
// find all nodes that have us as a neighbour
const neighbourNodeInfos = [];
for (const nodeThatMayHaveHeardUs of state.nodes) {
// find our node in this nodes neighbours
const nodeNeighbours = nodeThatMayHaveHeardUs.neighbours ?? [];
const neighbour = nodeNeighbours.find(function(neighbour) {
return neighbour.node_id.toString() === node.node_id.toString();
});
// we exist as a neighbour
if (neighbour) {
neighbourNodeInfos.push({
node: nodeThatMayHaveHeardUs,
neighbour: neighbour,
});
}
}
// ensure we have neighbours to show
if (neighbourNodeInfos.length === 0) {
return;
}
// add node neighbours
for (const neighbourNodeInfo of neighbourNodeInfos) {
const neighbourNode = neighbourNodeInfo.node;
const neighbour = neighbourNodeInfo.neighbour;
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
if (neighbour.snr === 0) {
continue;
}
// find neighbour node marker
const neighbourNodeMarker = findNodeMarkerById(neighbourNode.node_id);
if (!neighbourNodeMarker) {
continue;
}
// calculate distance in meters between nodes (rounded to 2 decimal places)
const distanceInMeters = neighbourNodeMarker.getLatLng().distanceTo(nodeMarker.getLatLng()).toFixed(2);
// don't show this neighbour connection if further than config allows
if (neighboursMaxDistance.value != null && parseFloat(distanceInMeters) > neighboursMaxDistance.value) {
continue;
}
// add neighbour line to map
const line = L.polyline([
nodeMarker.getLatLng(), // from us
neighbourNodeMarker.getLatLng(), // to neighbour
], {
color: getColourForSnr(neighbour.snr),
opacity: 1,
}).arrowheads({
size: '10px',
fill: true,
offsets: {
start: '25px',
end: '25px',
},
}).addTo(layerGroups.nodeNeighbors);
const tooltip = getNeighbourTooltipContent('theyHeard', node, neighbourNode, distanceInMeters, neighbour.snr);
line.bindTooltip(tooltip, {
sticky: true,
opacity: 1,
interactive: true,
}).bindPopup(tooltip).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
}
}
window.showNodeNeighboursThatWeHeard = function(id) {
cleanUpNodeNeighbors();
// find node
const node = findNodeById(id);
if (!node) {
return;
}
// find node marker
const nodeMarker = findNodeMarkerById(node.node_id);
if (!nodeMarker) {
return;
}
// show overlay
state.selectedNodeToShowNeighbours = node;
state.selectedNodeToShowNeighboursType = 'weHeard';
// ensure we have neighbours to show
const neighbours = node.neighbours ?? [];
if (neighbours.length === 0) {
return;
}
for (const neighbour of neighbours) {
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
if (neighbour.snr === 0) {
continue;
}
// find neighbor node
const neighbourNode = findNodeById(neighbour.node_id);
if (!neighbourNode) {
continue;
}
// find neighbor node marker
const neighbourNodeMarker = findNodeMarkerById(neighbour.node_id);
if (!neighbourNodeMarker) {
continue;
}
// calculate distance in meters between nodes (rounded to 2 decimal places)
const distanceInMeters = nodeMarker.getLatLng().distanceTo(neighbourNodeMarker.getLatLng()).toFixed(2);
if (neighboursMaxDistance.value != null && parseFloat(distanceInMeters) > neighboursMaxDistance.value) {
continue;
}
// add neighbour line to map
const line = L.polyline([
neighbourNodeMarker.getLatLng(), // from neighbor
nodeMarker.getLatLng(), // to us
], {
color: getColorForSnr(neighbour.snr),
opacity: 1,
}).arrowheads({
size: '10px',
fill: true,
offsets: {
start: '25px',
end: '25px',
},
}).addTo(layerGroups.nodeNeighbors);
const tooltip = getNeighbourTooltipContent('weHeard', node, neighbourNode, distanceInMeters, neighbour.snr);
line.bindTooltip(tooltip, {
sticky: true,
opacity: 1,
interactive: true,
}).bindPopup(tooltip).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
}
}
function onNodesUpdated(nodes) {
const now = moment();
state.nodes = [];
for (const node of nodes) {
// skip nodes older than configured node max age
if (nodesMaxAge.value) {
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
if (lastUpdatedAgeInMillis > nodesMaxAge.value * 1000) {
continue;
}
}
// skip nodes without position
if (!node.latitude || !node.longitude) {
continue;
}
// skip nodes with invalid position
if (isNaN(node.latitude) || isNaN(node.longitude)) {
continue;
}
// fix lat long
node.latitude = node.latitude / 10000000;
node.longitude = node.longitude / 10000000;
// wrap longitude for shortest path, everything to left of australia should be shown on the right
let longitude = parseFloat(node.longitude);
if (longitude <= 100) {
longitude += 360;
}
// icon based on mqtt connection state
let icon = icons.mqttDisconnected;
// use offline icon for nodes older than configured node offline age
if (nodesOfflineAge) {
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
if (lastUpdatedAgeInMillis > nodesOfflineAge.value * 1000) {
icon = icons.offline;
}
}
// determine if node was recently heard uplinking packets to mqtt
const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node);
if (nodeHasUplinkedToMqttRecently) {
icon = icons.mqttConnected;
}
// create node marker
const marker = L.marker([node.latitude, longitude], {
icon: icon,
tagName: node.node_id,
// we want to show online nodes above offline, but without needing to use separate layer groups
zIndexOffset: nodeHasUplinkedToMqttRecently ? 1000 : -1000,
}).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
// add marker to node layer groups
marker.addTo(layerGroups.nodes);
layerGroups.nodesClustered.addLayer(marker);
// add markers for routers and repeaters to routers layer group
if (node.role_name === 'ROUTER'
|| node.role_name === 'ROUTER_CLIENT'
|| node.role_name === 'ROUTER_LATE'
|| node.role_name === 'REPEATER') {
layerGroups.nodesRouter.addLayer(marker);
}
// show tooltip on desktop only
if (!isMobile()) {
marker.bindTooltip(getTooltipContentForNode(node), {
interactive: true,
});
}
// Push node and marker to cache
state.nodes.push(node);
state.nodeMarkers[node.node_id] = marker;
// show node info tooltip when clicking node marker
marker.on('click', function(event) {
// close all other popups and tooltips
closeAllTooltips();
closeAllPopups();
// find node
const node = findNodeById(event.target.options.tagName);
if (!node){
return;
}
// show position precision outline
showNodeOutline(node.node_id);
// open tooltip for node
getMap().openTooltip(getTooltipContentForNode(node), event.target.getLatLng(), {
interactive: true, // allow clicking buttons inside tooltip
permanent: true, // don't auto dismiss when clicking buttons inside tooltip
});
});
}
for(const node of nodes) {
// find current node
const currentNode = findNodeMarkerById(node.node_id);
if (!currentNode) {
continue;
}
// add node neighbours
var polylineOffset = 0;
const neighbours = node.neighbours ?? [];
for( const neighbour of neighbours) {
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
if(neighbour.snr === 0){
continue;
}
const neighbourNode = findNodeById(neighbour.node_id);
if (!neighbourNode) {
continue;
}
const neighbourNodeMarker = findNodeMarkerById(neighbour.node_id);
if (neighbourNodeMarker) {
// calculate distance in meters between nodes (rounded to 2 decimal places)
const distanceInMeters = currentNode.getLatLng().distanceTo(neighbourNodeMarker.getLatLng()).toFixed(2);
// don't show this neighbour connection if further than config allows
if (neighboursMaxDistance.value != null && parseFloat(distanceInMeters) > neighboursMaxDistance.value) {
continue;
}
// add neighbour line to map
const line = L.polyline([
currentNode.getLatLng(),
neighbourNodeMarker.getLatLng(),
], {
color: '#2563eb',
opacity: 0.75,
offset: polylineOffset,
}).addTo(layerGroups.neighbors);
// increase offset so next neighbour does not overlay other neighbours from self
polylineOffset += 2;
const tooltip = getNeighbourTooltipContent('weHeard', node, neighbourNode, distanceInMeters, neighbour.snr);
line.bindTooltip(tooltip, {
sticky: true,
opacity: 1,
interactive: true,
}).bindPopup(tooltip).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
}
}
}
}
function onWaypointsUpdated(waypoints) {
state.waypoints = [];
const now = moment();
// add nodes
for (const waypoint of waypoints) {
// skip waypoints older than configured waypoint max age
if (waypointsMaxAge.value) {
const lastUpdatedAgeInMillis = now.diff(moment(waypoint.updated_at));
if (lastUpdatedAgeInMillis > waypointsMaxAge.value * 1000) {
continue;
}
}
// skip expired waypoints
if (waypoint.expire < Date.now() / 1000) {
continue;
}
// skip waypoints without position
if (!waypoint.latitude || !waypoint.longitude) {
continue;
}
// skip nodes with invalid position
if (isNaN(waypoint.latitude) || isNaN(waypoint.longitude)) {
continue;
}
// fix lat long
waypoint.latitude = waypoint.latitude / 10000000;
waypoint.longitude = waypoint.longitude / 10000000;
// wrap longitude for shortest path, everything to left of australia should be shown on the right
let longitude = parseFloat(waypoint.longitude);
if (longitude <= 100) {
longitude += 360;
}
// determine emoji to show as marker icon
const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon;
const emojiText = String.fromCodePoint(emoji);
let tooltip = getTooltipContentForWaypoint(waypoint);
// create waypoint marker
const marker = L.marker([waypoint.latitude, longitude], {
icon: L.divIcon({
className: 'waypoint-label',
iconSize: [26, 26], // increase from 12px to 26px
html: emojiText,
}),
}).bindPopup(tooltip).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
// show tooltip on desktop only
if (!isMobile()) {
marker.bindTooltip(tooltip, {
interactive: true,
});
}
// add marker to waypoints layer groups
marker.addTo(layerGroups.waypoints)
// add to cache
state.waypoints.push(waypoint);
}
}
function onPositionHistoryUpdated(updatedPositionHistories) {
let positionHistoryLinesCords = [];
// add nodes
for (const positionHistory of updatedPositionHistories) {
// skip position history without position
if (!positionHistory.latitude || !positionHistory.longitude) {
continue;
}
// find node this position is for
const node = findNodeById(positionHistory.node_id);
if (!node) {
continue;
}
// skip position history without position
if (!positionHistory.latitude || !positionHistory.longitude) {
continue;
}
// skip nodes with invalid position
if (isNaN(positionHistory.latitude) || isNaN(positionHistory.longitude)) {
continue;
}
// fix lat long
positionHistory.latitude = positionHistory.latitude / 10000000;
positionHistory.longitude = positionHistory.longitude / 10000000;
// wrap longitude for shortest path, everything to left of australia should be shown on the right
let longitude = parseFloat(positionHistory.longitude);
if (longitude <= 100) {
longitude += 360;
}
positionHistoryLinesCords.push([positionHistory.latitude, longitude]);
let tooltip = "";
if(positionHistory.type === "position"){
tooltip += `<b>Position</b>`;
} else if(positionHistory.type === "map_report"){
tooltip += `<b>Map Report</b>`;
}
tooltip += `<br/>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}`;
tooltip += `<br/>${positionHistory.latitude}, ${positionHistory.longitude}`;
tooltip += `<br/>Heard on: ${moment(new Date(positionHistory.created_at)).format("DD/MM/YYYY hh:mm A")}`;
// add gateway info if available
if (positionHistory.gateway_id) {
const gatewayNode = findNodeById(positionHistory.gateway_id);
const gatewayNodeInfo = gatewayNode ? `[${gatewayNode.short_name}] ${gatewayNode.long_name}` : "???";
tooltip += `<br/>Heard by: <a href="javascript:void(0);" onclick="goToNode(${positionHistory.gateway_id})">${gatewayNodeInfo}</a>`;
}
// create position history marker
const marker = L.marker([positionHistory.latitude, longitude],{
icon: iconPositionHistory,
}).bindTooltip(tooltip).bindPopup(tooltip).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
// add marker to position history layer group
marker.addTo(layerGroups.nodePositionHistory);
}
// show lines between position history markers
L.polyline(positionHistoryLinesCords).addTo(layerGroups.nodePositionHistory);
}
function goToRandomNode() {
if (state.nodes.length > 0) {
const randomNode = state.nodes[Math.floor(Math.random() * state.nodes.length)];
if (randomNode) {
// go to node
if (goToNode(randomNode.node_id)) {
return;
}
// fallback to showing node details since we can't go to the node
window.showNodeDetails(randomNode.node_id);
}
}
}
function showNodePositionHistory(nodeId) {
// find node
const node = findNodeById(nodeId);
if (!node) {
return;
}
// update ui
state.selectedNode = null;
state.selectedNodeToShowPositionHistory = node;
state.positionHistoryModalExpanded = true;
// close node info tooltip as position history shows under it
closeAllTooltips();
// reset default time range when opening position history ui
// YYYY-MM-DDTHH:mm is the format expected by the datetime-local input type
state.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm');
state.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
// load position history
loadNodePositionHistory(nodeId);
}
function loadNodePositionHistory(nodeId) {
state.selectedNodePositionHistory = [];
axios.get(buildPath(`/api/v1/nodes/${nodeId}/position-history`), {
params: {
// parse from datetime-local format, and send as unix timestamp in milliseconds
time_from: moment(state.positionHistoryDateTimeFrom, "YYYY-MM-DDTHH:mm").format("x"),
time_to: moment(state.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"),
},
}).then((response) => {
state.selectedNodePositionHistory = response.data.position_history;
if (state.selectedNodeToShowPositionHistory != null) {
clearAllPositionHistory();
onPositionHistoryUpdated(response.data.position_history);
};
}).catch(() => {
// do nothing
});
}
function reload(goToNodeId, zoom) {
// show loading
state.loading = true;
// clear previous data
clearMap();
axios.get(buildPath('/api/v1/nodes')).then(response => {
// update nodes
onNodesUpdated(response.data.nodes);
// hide loading
state.loading = false;
// fetch waypoints (after awaiting nodes, so we can use nodes cache in waypoint tooltips)
axios.get(buildPath('/api/v1/waypoints')).then(response => {
onWaypointsUpdated(response.data.waypoints);
});
// go to node id if provided
if (goToNodeId) {
// go to node
if(goToNode(goToNodeId, false, zoom)) {
return;
}
// fallback to showing node details since we can't go to the node
window.showNodeDetails(goToNodeId);
}
});
}
function goToNode(id, animate, zoom){
// find node
const node = findNodeById(id);
if (!node) {
alert("Could not find node: " + id);
return false;
}
// find node marker by id
const nodeMarker = findNodeMarkerById(id);
if (!nodeMarker) {
return false;
}
// close all popups and tooltips
closeAllPopups();
closeAllTooltips();
// select node
showNodeOutline(id);
// fly to node marker
const shouldAnimate = animate != null ? animate : true;
getMap().flyTo(nodeMarker.getLatLng(), parseFloat(zoom || goToNodeZoomLevel.value), {
animate: enableMapAnimations.value ? shouldAnimate : false,
});
// open tooltip for node
getMap().openTooltip(getTooltipContentForNode(node), nodeMarker.getLatLng(), {
interactive: true, // allow clicking buttons inside tooltip
permanent: true, // don't auto dismiss when clicking buttons inside tooltip
});
// successfully went to node
return true;
}
function onSearchResultNodeClick(node) {
// clear search
state.searchText = '';
// hide search
state.mobileSearchVisible = false;
// go to node
if (goToNode(node.node_id) ){
return;
}
// fallback to showing node details since we can't go to the node
window.showNodeDetails(node.node_id);
}
onMounted(() => {
// set map bounds to be a little more than full size to prevent panning off screen
const bounds = [
[-100, 70], // top left
[100, 500], // bottom right
];
// create map positioned over AU and NZ
setMap(L.map(mapEl.value, {
maxBounds: bounds,
}));
// set view
getMap().setView([-15, 150], 2);
// remove leaflet link
getMap().attributionControl.setPrefix('');
// use tile layer based on config
const selectedTileLayer = tileLayers[selectedTileLayerName.value] || tileLayers['OpenStreetMap'];
selectedTileLayer.addTo(getMap());
// handle baselayerchange to update tile layer preference
getMap().on('baselayerchange', function(event) {
selectedTileLayerName.value = event.name;
});
// create legend
const legend = L.control({position: 'bottomleft'});
legend.onAdd = function (map) {
const div = L.DomUtil.create('div', 'leaflet-control-layers');
div.style.backgroundColor = 'white';
div.style.padding = '12px';
div.innerHTML = `<div style="margin-bottom:6px;"><strong>Legend</strong></div>`
+ `<div style="display:flex"><div class="icon-mqtt-connected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Connected</div>`
+ `<div style="display:flex"><div class="icon-mqtt-disconnected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Disconnected</div>`
+ `<div style="display:flex"><div class="icon-offline" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> Offline Too Long</div>`;
return div;
};
// handle adding/remove legend on map (can't use L.Control as an overlay, so we toggle an empty L.LayerGroup)
getMap().on('overlayadd overlayremove', function(event) {
if (event.name === 'Legend') {
if (event.type === 'overlayadd') {
getMap().addControl(legend);
} else if(event.type === 'overlayremove') {
getMap().removeControl(legend);
}
}
});
// add layers to control ui
L.control.groupedLayers(tileLayers, {
'Nodes': {
'All': layerGroups.nodes,
'Routers': layerGroups.nodesRouter,
'Clustered': layerGroups.nodesClustered,
'None': layerGroups.none,
},
'Overlays': {
'Legend': layerGroups.legend,
'Neighbors': layerGroups.neighbors,
'Waypoints': layerGroups.waypoints,
'Position History': layerGroups.nodePositionHistory,
},
}, {
// make the "Nodes" group exclusive (use radio inputs instead of checkbox)
exclusiveGroups: ['Nodes'],
}).addTo(getMap());
// enable base layers
layerGroups.nodesClustered.addTo(getMap());
// enable overlay layers based on config
if (enabledOverlayLayers.value.includes('Legend')) {
layerGroups.legend.addTo(getMap());
}
if (enabledOverlayLayers.value.includes('Neighbors')) {
layerGroups.neighbors.addTo(getMap());
}
if (enabledOverlayLayers.value.includes('Waypoints')) {
layerGroups.waypoints.addTo(getMap());
}
if (enabledOverlayLayers.value.includes('Position History')) {
layerGroups.nodePositionHistory.addTo(getMap());
}
// update config when map overlay is added/removed
getMap().on('overlayremove', function(event) {
const layerName = event.name;
enabledOverlayLayers.value = enabledOverlayLayers.value.filter(function(enabledOverlayLayer) {
return enabledOverlayLayer !== layerName;
});
});
getMap().on('overlayadd', function(event) {
const layerName = event.name;
if (!enabledOverlayLayers.value.includes(layerName)) {
enabledOverlayLayers.value.push(layerName);
}
});
getMap().on('click', function(event) {
// remove outline when map clicked
clearNodeOutline();
// clear search
state.searchText = '';
state.mobileSearchVisible = false;
// do nothing when clicking inside tooltip
const clickedElement = event.originalEvent.target;
if (elementOrAnyAncestorHasClass(clickedElement, 'leaflet-tooltip')) {
return;
}
closeAllTooltips();
closeAllPopups();
});
// auto update url when lat/lng/zoom changes
getMap().on('moveend zoomend', function() {
// check if user enabled auto updating position in url
if (!autoUpdatePositionInUrl.value) {
return;
}
// get map info
const latLng = getMap().getCenter();
const zoom = getMap().getZoom();
// construct new url
const url = new URL(window.location.href);
url.searchParams.set('lat', latLng.lat);
url.searchParams.set('lng', latLng.lng);
url.searchParams.set('zoom', zoom);
// update current url
if(window.history.replaceState){
window.history.replaceState(null, null, url.toString());
}
});
// parse url params
const queryParams = new URLSearchParams(location.search);
const queryNodeId = queryParams.get('node_id');
const queryLat = queryParams.get('lat');
const queryLng = queryParams.get('lng');
const queryZoom = queryParams.get('zoom');
// go to lat/lng if provided
if(queryLat && queryLng){
const zoomLevel = queryZoom || goToNodeZoomLevel.value
getMap().flyTo([queryLat, queryLng], parseFloat(zoomLevel), {
animate: false,
});
}
// reload and go to provided node id
reload(queryNodeId, queryZoom);
});
onMounted(() => {
if (lastSeenAnnouncementId.value !== CURRENT_ANNOUNCEMENT_ID) {
state.announcementVisible = true;
}
if (!isMobile() && hasSeenInfoModal.value === false) {
state.infoModalVisible = true;
}
})
</script>
<template>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="flex flex-col h-full">
<Announcement />
<Header
@reload="reload"
@random-node="goToRandomNode"
@search-click="onSearchResultNodeClick"
/>
<div id="map" style="width:100%;height:100%;" ref="appMap"></div>
</div>
</div>
<InfoModal />
<HardwareModelList />
<Settings />
<NodeInfo @show-position-history="showNodePositionHistory"/>
<NodeNeighborsModal @dismiss="cleanUpNodeNeighbors" />
<NodePositionHistoryModal @dismiss="cleanUpPositionHistory" />
<TracerouteInfo @go-to="goToNode" />
</template>

1
lora/.npmrc Normal file
View File

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

60
lora/LoraStream.js Normal file
View File

@ -0,0 +1,60 @@
import { Transform } from 'stream';
export default class LoraStream extends Transform {
constructor(options) {
super(options);
this.byteBuffer = new Uint8Array([]);
this.textDecoder = new TextDecoder();
}
_transform(chunk, encoding, callback) {
this.byteBuffer = new Uint8Array([
...this.byteBuffer,
...chunk,
]);
let processingExhausted = false;
while (this.byteBuffer.length !== 0 && !processingExhausted) {
const framingIndex = this.byteBuffer.findIndex((byte) => byte === 0x94);
const framingByte2 = this.byteBuffer[framingIndex + 1];
if (framingByte2 === 0xc3) {
if (this.byteBuffer.subarray(0, framingIndex).length) {
this.byteBuffer = this.byteBuffer.subarray(framingIndex);
}
const msb = this.byteBuffer[2];
const lsb = this.byteBuffer[3];
if (msb !== undefined && lsb !== undefined && this.byteBuffer.length >= 4 + (msb << 8) + lsb) {
const packet = this.byteBuffer.subarray(4, 4 + (msb << 8) + lsb);
const malformedDetectorIndex = packet.findIndex(
(byte) => byte === 0x94,
);
if (malformedDetectorIndex !== -1 && packet[malformedDetectorIndex + 1] === 0xc3) {
// malformed
this.byteBuffer = this.byteBuffer.subarray(malformedDetectorIndex);
} else {
this.byteBuffer = this.byteBuffer.subarray(3 + (msb << 8) + lsb + 1);
this.push(packet);
}
} else {
/** Only partioal message in buffer, wait for the rest */
processingExhausted = true;
}
} else {
/** Message not complete, only 1 byte in buffer */
processingExhausted = true;
}
}
callback();
}
_flush(callback) {
try {
if (this._buffer) {
this.push(this._buffer.trim());
}
callback();
} catch (err) {
callback(err);
}
}
}

62
lora/MeshtasticStream.js Normal file
View File

@ -0,0 +1,62 @@
import { Transform } from 'stream';
import { Mesh, Channel, Config, ModuleConfig } from '@meshtastic/protobufs';
import { fromBinary, create } from '@bufbuild/protobuf';
export default class MeshtasticStream extends Transform {
constructor(options) {
super({ readableObjectMode: true, ...options });
}
_transform(chunk, encoding, callback) {
const dataPacket = fromBinary(Mesh.FromRadioSchema, chunk);
let schema = null;
switch(dataPacket.payloadVariant.case) {
case 'packet':
schema = Mesh.MeshPacketSchema;
break;
case 'nodeInfo':
schema = Mesh.NodeInfoSchema;
break;
case 'myInfo':
schema = Mesh.MyNodeInfoSchema;
break;
case 'deviceuiConfig':
// can't find, come back to this
break;
case 'config':
schema = Config.ConfigSchema
break;
case 'moduleConfig':
schema = ModuleConfig.ModuleConfigSchema
break;
case 'fileInfo':
schema = Mesh.FileInfoSchema
break;
case 'channel':
schema = Channel.ChannelSchema
break;
case 'metadata':
schema = Mesh.DeviceMetadataSchema
break;
case 'logRecord':
schema = Mesh.LogRecordSchema
break;
case 'configCompleteId':
// done sending init data
break;
}
if (schema !== null) {
this.push(create(schema, dataPacket.payloadVariant.value));
}
callback();
}
_flush(callback) {
try {
if (this._buffer) {
this.push(this._buffer.trim());
}
callback();
} catch (err) {
callback(err);
}
}
}

107
lora/index.js Normal file
View File

@ -0,0 +1,107 @@
import { Mesh, Mqtt, Portnums, Telemetry, Config, Channel } from '@meshtastic/protobufs';
import { fromBinary, toBinary, create } from '@bufbuild/protobuf';
import { LoraStream } from './LoraStream';
import { MeshtasticStream } from './MeshtasticStream';
import net from 'net';
const client = new net.Socket();
const IP = '192.168.10.117';
const PORT = 4403;
function sendHello() {
const data = create(Mesh.ToRadioSchema, {
payloadVariant: {
case: 'wantConfigId',
value: 1,
}
});
sendToDevice(toBinary(Mesh.ToRadioSchema, data));
}
function sendToDevice(data) {
const bufferLength = data.length;
const header = new Uint8Array([
0x94,
0xC3,
(bufferLength >> 8) & 0xFF,
bufferLength & 0xFF,
]);
data = new Uint8Array([...header, ...data]);
client.write(data);
}
client.connect(PORT, IP, function() {
console.log('Connected');
sendHello();
});
const meshtasticStream = new MeshtasticStream();
client.pipe(new LoraStream()).pipe(meshtasticStream);
meshtasticStream.on('data', (data) => {
parseMeshtastic(data['$typeName'], data);
})
function parseMeshtastic(typeName, data) {
switch(typeName) {
case Mesh.MeshPacketSchema.typeName:
onMeshPacket(data);
break;
case Mesh.NodeInfoSchema.typeName:
console.log('node info');
break;
}
}
function onMeshPacket(envelope) {
const payloadVariant = envelope.payloadVariant.case;
if (payloadVariant === 'encrypted') {
// attempt decryption
}
const dataPacket = envelope.payloadVariant.value;
const portNum = dataPacket.portnum;
if (!portNum) {
return;
}
let schema = null;
switch (portNum) {
case Portnums.PortNum.POSITION_APP:
schema = Mesh.PositionSchema;
break;
case Portnums.PortNum.TELEMETRY_APP:
schema = Telemetry.TelemetrySchema;
break;
case Portnums.PortNum.TEXT_MESSAGE_APP:
// no schema?
break;
case Portnums.PortNum.WAYPOINT_APP:
schema = Mesh.WaypointSchema;
break;
case Portnums.PortNum.TRACEROUTE_APP:
schema = Mesh.RouteDiscoverySchema;
break;
case Portnums.PortNum.NODEINFO_APP:
schema = Mesh.NodeInfoSchema;
break;
case Portnums.PortNum.NEIGHBORINFO_APP:
schema = Mesh.NeighborInfoSchema;
break;
case Portnums.PortNum.MAP_REPORT_APP:
schema = Mqtt.MapReportSchema;
break;
}
let decodedData = dataPacket.payload;
if (schema !== null) {
try {
decodedData = fromBinary(schema, decodedData);
console.log(decodedData);
} catch(e) {
// ignore errors, likely incomplete data
return;
}
}
}

341
lora/package-lock.json generated Normal file
View File

@ -0,0 +1,341 @@
{
"name": "lora",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lora",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.2.5",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
"@prisma/client": "^5.11.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^7.0.1"
},
"devDependencies": {
"prisma": "^5.10.2"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz",
"integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@meshtastic/protobufs": {
"name": "@jsr/meshtastic__protobufs",
"version": "2.6.2",
"resolved": "https://npm.jsr.io/~/11/@jsr/meshtastic__protobufs/2.6.2.tgz",
"integrity": "sha512-bIENtFnUEru28GrAeSdiBS9skp0hN/3HZunMbF/IjvUrXOlx2fptKVj3b+pzjOWnLBZxllrByV/W+XDmrxqJ6g==",
"dependencies": {
"@bufbuild/protobuf": "^2.2.3"
}
},
"node_modules/@prisma/client": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=16.13"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
}
},
"node_modules/@prisma/debug": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0",
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"@prisma/fetch-engine": "5.22.0",
"@prisma/get-platform": "5.22.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0",
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"@prisma/get-platform": "5.22.0"
}
},
"node_modules/@prisma/get-platform": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/array-back": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
"integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk-template": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/chalk-template?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/command-line-args": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz",
"integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==",
"license": "MIT",
"dependencies": {
"array-back": "^3.1.0",
"find-replace": "^3.0.0",
"lodash.camelcase": "^4.3.0",
"typical": "^4.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/command-line-usage": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz",
"integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==",
"license": "MIT",
"dependencies": {
"array-back": "^6.2.2",
"chalk-template": "^0.4.0",
"table-layout": "^4.1.0",
"typical": "^7.1.1"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/command-line-usage/node_modules/array-back": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz",
"integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/command-line-usage/node_modules/typical": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz",
"integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/find-replace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
"integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==",
"license": "MIT",
"dependencies": {
"array-back": "^3.0.1"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/prisma": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/engines": "5.22.0"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=16.13"
},
"optionalDependencies": {
"fsevents": "2.3.3"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/table-layout": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz",
"integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==",
"license": "MIT",
"dependencies": {
"array-back": "^6.2.2",
"wordwrapjs": "^5.1.0"
},
"engines": {
"node": ">=12.17"
}
},
"node_modules/table-layout/node_modules/array-back": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz",
"integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/typical": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
"integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wordwrapjs": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz",
"integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
}
}
}

19
lora/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "lora",
"version": "1.0.0",
"main": "index.js",
"author": "",
"license": "ISC",
"description": "",
"type": "module",
"dependencies": {
"@bufbuild/protobuf": "^2.2.5",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
"@prisma/client": "^5.11.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^7.0.1"
},
"devDependencies": {
"prisma": "^5.10.2"
}
}

View File

@ -1,39 +0,0 @@
[Unit]
Description=meshtastic-map-mqtt
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
User=liamcottle
WorkingDirectory=/home/liamcottle/meshtastic-map
ExecStart=/usr/bin/env node /home/liamcottle/meshtastic-map/src/mqtt.js \
--mqtt-broker-url mqtt://127.0.0.1 \
--mqtt-username username \
--mqtt-password password \
--mqtt-client-id meshtastic.example.com \
--mqtt-topic 'msh/#' \
--collect-positions \
--collect-text-messages \
--collect-waypoints \
--ignore-direct-messages \
--purge-interval-seconds 60 \
--purge-nodes-unheard-for-seconds 604800 \
--purge-device-metrics-after-seconds 604800 \
--purge-environment-metrics-after-seconds 604800 \
--purge-map-reports-after-seconds 604800 \
--purge-neighbour-infos-after-seconds 604800 \
--purge-power-metrics-after-seconds 604800 \
--purge-positions-after-seconds 604800 \
--purge-service-envelopes-after-seconds 604800 \
--purge-text-messages-after-seconds 604800 \
--purge-traceroutes-after-seconds 604800 \
--purge-waypoints-after-seconds 604800 \
--forget-outdated-node-positions-after-seconds 604800 \
--drop-packets-not-ok-to-mqtt \
--old-firmware-position-precision 16
[Install]
WantedBy=multi-user.target

View File

@ -1,15 +0,0 @@
[Unit]
Description=meshtastic-map
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
User=liamcottle
WorkingDirectory=/home/liamcottle/meshtastic-map
ExecStart=/usr/bin/env node /home/liamcottle/meshtastic-map/src/index.js
[Install]
WantedBy=multi-user.target

1
mqtt/.npmrc Normal file
View File

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

11
mqtt/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM node:lts-alpine3.17
# add project files to /app
ADD ./mqtt /app
ADD ./common /app
WORKDIR /app
# install node dependencies
RUN npm install && npx prisma generate
ENTRYPOINT ["/app/entrypoint.sh"]

1399
mqtt/index.js Normal file

File diff suppressed because it is too large Load Diff

877
mqtt/package-lock.json generated Normal file
View File

@ -0,0 +1,877 @@
{
"name": "mqtt",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mqtt",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.2.5",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
"@prisma/client": "^5.11.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^7.0.1",
"mqtt": "^5.11.0"
},
"devDependencies": {
"prisma": "^5.10.2"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz",
"integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@meshtastic/protobufs": {
"name": "@jsr/meshtastic__protobufs",
"version": "2.6.2",
"resolved": "https://npm.jsr.io/~/11/@jsr/meshtastic__protobufs/2.6.2.tgz",
"integrity": "sha512-bIENtFnUEru28GrAeSdiBS9skp0hN/3HZunMbF/IjvUrXOlx2fptKVj3b+pzjOWnLBZxllrByV/W+XDmrxqJ6g==",
"dependencies": {
"@bufbuild/protobuf": "^2.2.3"
}
},
"node_modules/@prisma/client": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=16.13"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
}
},
"node_modules/@prisma/debug": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0",
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"@prisma/fetch-engine": "5.22.0",
"@prisma/get-platform": "5.22.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0",
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"@prisma/get-platform": "5.22.0"
}
},
"node_modules/@prisma/get-platform": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "5.22.0"
}
},
"node_modules/@types/node": {
"version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/readable-stream": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz",
"integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"safe-buffer": "~5.1.1"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/array-back": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
"integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bl": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz",
"integrity": "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==",
"license": "MIT",
"dependencies": {
"@types/readable-stream": "^4.0.0",
"buffer": "^6.0.3",
"inherits": "^2.0.4",
"readable-stream": "^4.2.0"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk-template": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/chalk-template?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/command-line-args": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz",
"integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==",
"license": "MIT",
"dependencies": {
"array-back": "^3.1.0",
"find-replace": "^3.0.0",
"lodash.camelcase": "^4.3.0",
"typical": "^4.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/command-line-usage": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz",
"integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==",
"license": "MIT",
"dependencies": {
"array-back": "^6.2.2",
"chalk-template": "^0.4.0",
"table-layout": "^4.1.0",
"typical": "^7.1.1"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/command-line-usage/node_modules/array-back": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz",
"integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/command-line-usage/node_modules/typical": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz",
"integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/commist": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
"integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
"license": "MIT"
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fast-unique-numbers": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz",
"integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.8",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=16.1.0"
}
},
"node_modules/find-replace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
"integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==",
"license": "MIT",
"dependencies": {
"array-back": "^3.0.1"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"license": "MIT"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"license": "MIT",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/js-sdsl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mqtt": {
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.11.0.tgz",
"integrity": "sha512-VDqfADTNvohwcY02NgxPb7OojIeDrNQ1q62r/DcM+bnIWY8LBi3nMTvdEaFEp6Bu4ejBIpHjJVthUEgnvGLemA==",
"license": "MIT",
"dependencies": {
"@types/readable-stream": "^4.0.18",
"@types/ws": "^8.5.14",
"commist": "^3.2.0",
"concat-stream": "^2.0.0",
"debug": "^4.4.0",
"help-me": "^5.0.0",
"lru-cache": "^10.4.3",
"minimist": "^1.2.8",
"mqtt-packet": "^9.0.2",
"number-allocator": "^1.0.14",
"readable-stream": "^4.7.0",
"reinterval": "^1.1.0",
"rfdc": "^1.4.1",
"socks": "^2.8.3",
"split2": "^4.2.0",
"worker-timers": "^7.1.8",
"ws": "^8.18.0"
},
"bin": {
"mqtt": "build/bin/mqtt.js",
"mqtt_pub": "build/bin/pub.js",
"mqtt_sub": "build/bin/sub.js"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/mqtt-packet": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
"license": "MIT",
"dependencies": {
"bl": "^6.0.8",
"debug": "^4.3.4",
"process-nextick-args": "^2.0.1"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/number-allocator": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
"integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.1",
"js-sdsl": "4.3.0"
}
},
"node_modules/prisma": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/engines": "5.22.0"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=16.13"
},
"optionalDependencies": {
"fsevents": "2.3.3"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/reinterval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz",
"integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==",
"license": "MIT"
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
"license": "MIT",
"dependencies": {
"ip-address": "^9.0.5",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/table-layout": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz",
"integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==",
"license": "MIT",
"dependencies": {
"array-back": "^6.2.2",
"wordwrapjs": "^5.1.0"
},
"engines": {
"node": ">=12.17"
}
},
"node_modules/table-layout/node_modules/array-back": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz",
"integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typical": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
"integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/wordwrapjs": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz",
"integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/worker-timers": {
"version": "7.1.8",
"resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz",
"integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.5",
"tslib": "^2.6.2",
"worker-timers-broker": "^6.1.8",
"worker-timers-worker": "^7.0.71"
}
},
"node_modules/worker-timers-broker": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz",
"integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.5",
"fast-unique-numbers": "^8.0.13",
"tslib": "^2.6.2",
"worker-timers-worker": "^7.0.71"
}
},
"node_modules/worker-timers-worker": {
"version": "7.0.71",
"resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz",
"integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.5",
"tslib": "^2.6.2"
}
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

20
mqtt/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "mqtt",
"version": "1.0.0",
"main": "index.js",
"author": "",
"license": "ISC",
"description": "",
"type": "module",
"dependencies": {
"@bufbuild/protobuf": "^2.2.5",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.6.2",
"@prisma/client": "^5.11.0",
"command-line-args": "^5.2.1",
"command-line-usage": "^7.0.1",
"mqtt": "^5.11.0"
},
"devDependencies": {
"prisma": "^5.10.2"
}
}

View File

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

View File

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

5057
package-lock.json generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +0,0 @@
*AdminMessage.payload_variant anonymous_oneof:true
*AdminMessage.set_canned_message_module_messages max_size:201
*AdminMessage.get_canned_message_module_messages_response max_size:201
*AdminMessage.delete_file_request max_size:201
*AdminMessage.set_ringtone_message max_size:231
*AdminMessage.get_ringtone_response max_size:231
*HamParameters.call_sign max_size:8
*HamParameters.short_name max_size:6
*NodeRemoteHardwarePinsResponse.node_remote_hardware_pins max_count:16

View File

@ -1,364 +0,0 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/channel.proto";
import "meshtastic/config.proto";
import "meshtastic/connection_status.proto";
import "meshtastic/deviceonly.proto";
import "meshtastic/mesh.proto";
import "meshtastic/module_config.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "AdminProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This message is handled by the Admin module and is responsible for all settings/channel read/write operations.
* This message is used to do settings operations to both remote AND local nodes.
* (Prior to 1.2 these operations were done via special ToRadio operations)
*/
message AdminMessage {
/*
* TODO: REPLACE
*/
enum ConfigType {
/*
* TODO: REPLACE
*/
DEVICE_CONFIG = 0;
/*
* TODO: REPLACE
*/
POSITION_CONFIG = 1;
/*
* TODO: REPLACE
*/
POWER_CONFIG = 2;
/*
* TODO: REPLACE
*/
NETWORK_CONFIG = 3;
/*
* TODO: REPLACE
*/
DISPLAY_CONFIG = 4;
/*
* TODO: REPLACE
*/
LORA_CONFIG = 5;
/*
* TODO: REPLACE
*/
BLUETOOTH_CONFIG = 6;
}
/*
* TODO: REPLACE
*/
enum ModuleConfigType {
/*
* TODO: REPLACE
*/
MQTT_CONFIG = 0;
/*
* TODO: REPLACE
*/
SERIAL_CONFIG = 1;
/*
* TODO: REPLACE
*/
EXTNOTIF_CONFIG = 2;
/*
* TODO: REPLACE
*/
STOREFORWARD_CONFIG = 3;
/*
* TODO: REPLACE
*/
RANGETEST_CONFIG = 4;
/*
* TODO: REPLACE
*/
TELEMETRY_CONFIG = 5;
/*
* TODO: REPLACE
*/
CANNEDMSG_CONFIG = 6;
/*
* TODO: REPLACE
*/
AUDIO_CONFIG = 7;
/*
* TODO: REPLACE
*/
REMOTEHARDWARE_CONFIG = 8;
/*
* TODO: REPLACE
*/
NEIGHBORINFO_CONFIG = 9;
/*
* TODO: REPLACE
*/
AMBIENTLIGHTING_CONFIG = 10;
/*
* TODO: REPLACE
*/
DETECTIONSENSOR_CONFIG = 11;
/*
* TODO: REPLACE
*/
PAXCOUNTER_CONFIG = 12;
}
/*
* TODO: REPLACE
*/
oneof payload_variant {
/*
* Send the specified channel in the response to this message
* NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present)
*/
uint32 get_channel_request = 1;
/*
* TODO: REPLACE
*/
Channel get_channel_response = 2;
/*
* Send the current owner data in the response to this message.
*/
bool get_owner_request = 3;
/*
* TODO: REPLACE
*/
User get_owner_response = 4;
/*
* Ask for the following config data to be sent
*/
ConfigType get_config_request = 5;
/*
* Send the current Config in the response to this message.
*/
Config get_config_response = 6;
/*
* Ask for the following config data to be sent
*/
ModuleConfigType get_module_config_request = 7;
/*
* Send the current Config in the response to this message.
*/
ModuleConfig get_module_config_response = 8;
/*
* Get the Canned Message Module messages in the response to this message.
*/
bool get_canned_message_module_messages_request = 10;
/*
* Get the Canned Message Module messages in the response to this message.
*/
string get_canned_message_module_messages_response = 11;
/*
* Request the node to send device metadata (firmware, protobuf version, etc)
*/
bool get_device_metadata_request = 12;
/*
* Device metadata response
*/
DeviceMetadata get_device_metadata_response = 13;
/*
* Get the Ringtone in the response to this message.
*/
bool get_ringtone_request = 14;
/*
* Get the Ringtone in the response to this message.
*/
string get_ringtone_response = 15;
/*
* Request the node to send it's connection status
*/
bool get_device_connection_status_request = 16;
/*
* Device connection status response
*/
DeviceConnectionStatus get_device_connection_status_response = 17;
/*
* Setup a node for licensed amateur (ham) radio operation
*/
HamParameters set_ham_mode = 18;
/*
* Get the mesh's nodes with their available gpio pins for RemoteHardware module use
*/
bool get_node_remote_hardware_pins_request = 19;
/*
* Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use
*/
NodeRemoteHardwarePinsResponse get_node_remote_hardware_pins_response = 20;
/*
* Enter (UF2) DFU mode
* Only implemented on NRF52 currently
*/
bool enter_dfu_mode_request = 21;
/*
* Delete the file by the specified path from the device
*/
string delete_file_request = 22;
/*
* Set the owner for this node
*/
User set_owner = 32;
/*
* Set channels (using the new API).
* A special channel is the "primary channel".
* The other records are secondary channels.
* Note: only one channel can be marked as primary.
* If the client sets a particular channel to be primary, the previous channel will be set to SECONDARY automatically.
*/
Channel set_channel = 33;
/*
* Set the current Config
*/
Config set_config = 34;
/*
* Set the current Config
*/
ModuleConfig set_module_config = 35;
/*
* Set the Canned Message Module messages text.
*/
string set_canned_message_module_messages = 36;
/*
* Set the ringtone for ExternalNotification.
*/
string set_ringtone_message = 37;
/*
* Remove the node by the specified node-num from the NodeDB on the device
*/
uint32 remove_by_nodenum = 38;
/*
* Begins an edit transaction for config, module config, owner, and channel settings changes
* This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings)
*/
bool begin_edit_settings = 64;
/*
* Commits an open transaction for any edits made to config, module config, owner, and channel settings
*/
bool commit_edit_settings = 65;
/*
* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot)
* Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth.
*/
int32 reboot_ota_seconds = 95;
/*
* This message is only supported for the simulator Portduino build.
* If received the simulator will exit successfully.
*/
bool exit_simulator = 96;
/*
* Tell the node to reboot in this many seconds (or <0 to cancel reboot)
*/
int32 reboot_seconds = 97;
/*
* Tell the node to shutdown in this many seconds (or <0 to cancel shutdown)
*/
int32 shutdown_seconds = 98;
/*
* Tell the node to factory reset, all device settings will be returned to factory defaults.
*/
int32 factory_reset = 99;
/*
* Tell the node to reset the nodedb.
*/
int32 nodedb_reset = 100;
}
}
/*
* Parameters for setting up Meshtastic for ameteur radio usage
*/
message HamParameters {
/*
* Amateur radio call sign, eg. KD2ABC
*/
string call_sign = 1;
/*
* Transmit power in dBm at the LoRA transceiver, not including any amplification
*/
int32 tx_power = 2;
/*
* The selected frequency of LoRA operation
* Please respect your local laws, regulations, and band plans.
* Ensure your radio is capable of operating of the selected frequency before setting this.
*/
float frequency = 3;
/*
* Optional short name of user
*/
string short_name = 4;
}
/*
* Response envelope for node_remote_hardware_pins
*/
message NodeRemoteHardwarePinsResponse {
/*
* Nodes and their respective remote hardware GPIO pins
*/
repeated NodeRemoteHardwarePin node_remote_hardware_pins = 1;
}

View File

@ -1 +0,0 @@
*ChannelSet.settings max_count:8

View File

@ -1,31 +0,0 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/channel.proto";
import "meshtastic/config.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "AppOnlyProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This is the most compact possible representation for a set of channels.
* It includes only one PRIMARY channel (which must be first) and
* any SECONDARY channels.
* No DISABLED channels are included.
* This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL
*/
message ChannelSet {
/*
* Channel list with settings
*/
repeated ChannelSettings settings = 1;
/*
* LoRa config
*/
Config.LoRaConfig lora_config = 2;
}

View File

@ -1,6 +0,0 @@
*Contact.callsign max_size:120
*Contact.device_callsign max_size:120
*Status.battery int_size:8
*PLI.course int_size:16
*GeoChat.message max_size:200
*GeoChat.to max_size:120

View File

@ -1,251 +0,0 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ATAKProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Packets for the official ATAK Plugin
*/
message TAKPacket
{
/*
* Are the payloads strings compressed for LoRA transport?
*/
bool is_compressed = 1;
/*
* The contact / callsign for ATAK user
*/
Contact contact = 2;
/*
* The group for ATAK user
*/
Group group = 3;
/*
* The status of the ATAK EUD
*/
Status status = 4;
/*
* The payload of the packet
*/
oneof payload_variant {
/*
* TAK position report
*/
PLI pli = 5;
/*
* ATAK GeoChat message
*/
GeoChat chat = 6;
}
}
/*
* ATAK GeoChat message
*/
message GeoChat {
/*
* The text message
*/
string message = 1;
/*
* Uid recipient of the message
*/
optional string to = 2;
}
/*
* ATAK Group
* <__group role='Team Member' name='Cyan'/>
*/
message Group {
/*
* Role of the group member
*/
MemberRole role = 1;
/*
* Team (color)
* Default Cyan
*/
Team team = 2;
}
enum Team {
/*
* Unspecifed
*/
Unspecifed_Color = 0;
/*
* White
*/
White = 1;
/*
* Yellow
*/
Yellow = 2;
/*
* Orange
*/
Orange = 3;
/*
* Magenta
*/
Magenta = 4;
/*
* Red
*/
Red = 5;
/*
* Maroon
*/
Maroon = 6;
/*
* Purple
*/
Purple = 7;
/*
* Dark Blue
*/
Dark_Blue = 8;
/*
* Blue
*/
Blue = 9;
/*
* Cyan
*/
Cyan = 10;
/*
* Teal
*/
Teal = 11;
/*
* Green
*/
Green = 12;
/*
* Dark Green
*/
Dark_Green = 13;
/*
* Brown
*/
Brown = 14;
}
/*
* Role of the group member
*/
enum MemberRole {
/*
* Unspecifed
*/
Unspecifed = 0;
/*
* Team Member
*/
TeamMember = 1;
/*
* Team Lead
*/
TeamLead = 2;
/*
* Headquarters
*/
HQ = 3;
/*
* Airsoft enthusiast
*/
Sniper = 4;
/*
* Medic
*/
Medic = 5;
/*
* ForwardObserver
*/
ForwardObserver = 6;
/*
* Radio Telephone Operator
*/
RTO = 7;
/*
* Doggo
*/
K9 = 8;
}
/*
* ATAK EUD Status
* <status battery='100' />
*/
message Status {
/*
* Battery level
*/
uint32 battery = 1;
}
/*
* ATAK Contact
* <contact endpoint='0.0.0.0:4242:tcp' phone='+12345678' callsign='FALKE'/>
*/
message Contact {
/*
* Callsign
*/
string callsign = 1;
/*
* Device callsign
*/
string device_callsign = 2;
/*
* IP address of endpoint in integer form (0.0.0.0 default)
*/
// fixed32 enpoint_address = 3;
/*
* Port of endpoint (4242 default)
*/
// uint32 endpoint_port = 4;
/*
* Phone represented as integer
* Terrible practice, but we really need the wire savings
*/
// uint32 phone = 4;
}
/*
* Position Location Information from ATAK
*/
message PLI {
/*
* The new preferred location encoding, multiply by 1e-7 to get degrees
* in floating point
*/
sfixed32 latitude_i = 1;
/*
* The new preferred location encoding, multiply by 1e-7 to get degrees
* in floating point
*/
sfixed32 longitude_i = 2;
/*
* Altitude (ATAK prefers HAE)
*/
int32 altitude = 3;
/*
* Speed
*/
uint32 speed = 4;
/*
* Course in degrees
*/
uint32 course = 5;
}

View File

@ -1 +0,0 @@
*CannedMessageModuleConfig.messages max_size:201

View File

@ -1,19 +0,0 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "CannedMessageConfigProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Canned message module configuration.
*/
message CannedMessageModuleConfig {
/*
* Predefined messages for canned message module separated by '|' characters.
*/
string messages = 1;
}

View File

@ -1,5 +0,0 @@
*Channel.index int_size:8
# 256 bit or 128 bit psk key
*ChannelSettings.psk max_size:32
*ChannelSettings.name max_size:12

View File

@ -1,150 +0,0 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ChannelProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This information can be encoded as a QRcode/url so that other users can configure
* their radio to join the same channel.
* A note about how channel names are shown to users: channelname-X
* poundsymbol is a prefix used to indicate this is a channel name (idea from @professr).
* Where X is a letter from A-Z (base 26) representing a hash of the PSK for this
* channel - so that if the user changes anything about the channel (which does
* force a new PSK) this letter will also change. Thus preventing user confusion if
* two friends try to type in a channel name of "BobsChan" and then can't talk
* because their PSKs will be different.
* The PSK is hashed into this letter by "0x41 + [xor all bytes of the psk ] modulo 26"
* This also allows the option of someday if people have the PSK off (zero), the
* users COULD type in a channel name and be able to talk.
* FIXME: Add description of multi-channel support and how primary vs secondary channels are used.
* FIXME: explain how apps use channels for security.
* explain how remote settings and remote gpio are managed as an example
*/
message ChannelSettings {
/*
* Deprecated in favor of LoraConfig.channel_num
*/
uint32 channel_num = 1 [deprecated = true];
/*
* A simple pre-shared key for now for crypto.
* Must be either 0 bytes (no crypto), 16 bytes (AES128), or 32 bytes (AES256).
* A special shorthand is used for 1 byte long psks.
* These psks should be treated as only minimally secure,
* because they are listed in this source code.
* Those bytes are mapped using the following scheme:
* `0` = No crypto
* `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}
* `2` through 10 = The default channel key, except with 1 through 9 added to the last byte.
* Shown to user as simple1 through 10
*/
bytes psk = 2;
/*
* A SHORT name that will be packed into the URL.
* Less than 12 bytes.
* Something for end users to call the channel
* If this is the empty string it is assumed that this channel
* is the special (minimally secure) "Default"channel.
* In user interfaces it should be rendered as a local language translation of "X".
* For channel_num hashing empty string will be treated as "X".
* Where "X" is selected based on the English words listed above for ModemPreset
*/
string name = 3;
/*
* Used to construct a globally unique channel ID.
* The full globally unique ID will be: "name.id" where ID is shown as base36.
* Assuming that the number of meshtastic users is below 20K (true for a long time)
* the chance of this 64 bit random number colliding with anyone else is super low.
* And the penalty for collision is low as well, it just means that anyone trying to decrypt channel messages might need to
* try multiple candidate channels.
* Any time a non wire compatible change is made to a channel, this field should be regenerated.
* There are a small number of 'special' globally known (and fairly) insecure standard channels.
* Those channels do not have a numeric id included in the settings, but instead it is pulled from
* a table of well known IDs.
* (see Well Known Channels FIXME)
*/
fixed32 id = 4;
/*
* If true, messages on the mesh will be sent to the *public* internet by any gateway ndoe
*/
bool uplink_enabled = 5;
/*
* If true, messages seen on the internet will be forwarded to the local mesh.
*/
bool downlink_enabled = 6;
/*
* Per-channel module settings.
*/
ModuleSettings module_settings = 7;
}
/*
* This message is specifically for modules to store per-channel configuration data.
*/
message ModuleSettings {
/*
* Bits of precision for the location sent in position packets.
*/
uint32 position_precision = 1;
}
/*
* A pair of a channel number, mode and the (sharable) settings for that channel
*/
message Channel {
/*
* How this channel is being used (or not).
* Note: this field is an enum to give us options for the future.
* In particular, someday we might make a 'SCANNING' option.
* SCANNING channels could have different frequencies and the radio would
* occasionally check that freq to see if anything is being transmitted.
* For devices that have multiple physical radios attached, we could keep multiple PRIMARY/SCANNING channels active at once to allow
* cross band routing as needed.
* If a device has only a single radio (the common case) only one channel can be PRIMARY at a time
* (but any number of SECONDARY channels can't be sent received on that common frequency)
*/
enum Role {
/*
* This channel is not in use right now
*/
DISABLED = 0;
/*
* This channel is used to set the frequency for the radio - all other enabled channels must be SECONDARY
*/
PRIMARY = 1;
/*
* Secondary channels are only used for encryption/decryption/authentication purposes.
* Their radio settings (freq etc) are ignored, only psk is used.
*/
SECONDARY = 2;
}
/*
* The index of this channel in the channel table (from 0 to MAX_NUM_CHANNELS-1)
* (Someday - not currently implemented) An index of -1 could be used to mean "set by name",
* in which case the target node will find and set the channel by settings.name.
*/
int32 index = 1;
/*
* The new settings, or NULL to disable that channel
*/
ChannelSettings settings = 2;
/*
* TODO: REPLACE
*/
Role role = 3;
}

View File

@ -1,2 +0,0 @@
*DeviceProfile.long_name max_size:40
*DeviceProfile.short_name max_size:5

View File

@ -1,50 +0,0 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/localonly.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ClientOnlyProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This abstraction is used to contain any configuration for provisioning a node on any client.
* It is useful for importing and exporting configurations.
*/
message DeviceProfile {
/*
* Long name for the node
*/
optional string long_name = 1;
/*
* Short name of the node
*/
optional string short_name = 2;
/*
* The url of the channels from our node
*/
optional string channel_url = 3;
/*
* The Config of the node
*/
optional LocalConfig config = 4;
/*
* The ModuleConfig of the node
*/
optional LocalModuleConfig module_config = 5;
}
/*
* A heartbeat message is sent by a node to indicate that it is still alive.
* This is currently only needed to keep serial connections alive.
*/
message Heartbeat {
}

View File

@ -1,14 +0,0 @@
*NetworkConfig.wifi_ssid max_size:33
*NetworkConfig.wifi_psk max_size:65
*NetworkConfig.ntp_server max_size:33
*NetworkConfig.rsyslog_server max_size:33
# Max of three ignored nodes for our testing
*LoRaConfig.ignore_incoming max_count:3
*LoRaConfig.tx_power int_size:8
*LoRaConfig.bandwidth int_size:16
*LoRaConfig.coding_rate int_size:8
*LoRaConfig.channel_num int_size:16
*PowerConfig.device_battery_ina_address int_size:8

View File

@ -1,986 +0,0 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ConfigProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
message Config {
/*
* Configuration
*/
message DeviceConfig {
/*
* Defines the device's role on the Mesh network
*/
enum Role {
/*
* Description: App connected or stand alone messaging device.
* Technical Details: Default Role
*/
CLIENT = 0;
/*
* Description: Device that does not forward packets from other devices.
*/
CLIENT_MUTE = 1;
/*
* Description: Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list.
* Technical Details: Mesh packets will prefer to be routed over this node. This node will not be used by client apps.
* The wifi radio and the oled screen will be put to sleep.
* This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh.
*/
ROUTER = 2;
/*
* Description: Combination of both ROUTER and CLIENT. Not for mobile devices.
*/
ROUTER_CLIENT = 3;
/*
* Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
* Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
* or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
*/
REPEATER = 4;
/*
* Description: Broadcasts GPS position packets as priority.
* Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default.
* When used in conjunction with power.is_power_saving = true, nodes will wake up,
* send position, and then sleep for position.position_broadcast_secs seconds.
*/
TRACKER = 5;
/*
* Description: Broadcasts telemetry packets as priority.
* Technical Details: Telemetry Mesh packets will be prioritized higher and sent more frequently by default.
* When used in conjunction with power.is_power_saving = true, nodes will wake up,
* send environment telemetry, and then sleep for telemetry.environment_update_interval seconds.
*/
SENSOR = 6;
/*
* Description: Optimized for ATAK system communication and reduces routine broadcasts.
* Technical Details: Used for nodes dedicated for connection to an ATAK EUD.
* Turns off many of the routine broadcasts to favor CoT packet stream
* from the Meshtastic ATAK plugin -> IMeshService -> Node
*/
TAK = 7;
/*
* Description: Device that only broadcasts as needed for stealth or power savings.
* Technical Details: Used for nodes that "only speak when spoken to"
* Turns all of the routine broadcasts but allows for ad-hoc communication
* Still rebroadcasts, but with local only rebroadcast mode (known meshes only)
* Can be used for clandestine operation or to dramatically reduce airtime / power consumption
*/
CLIENT_HIDDEN = 8;
/*
* Description: Broadcasts location as message to default channel regularly for to assist with device recovery.
* Technical Details: Used to automatically send a text message to the mesh
* with the current position of the device on a frequent interval:
* "I'm lost! Position: lat / long"
*/
LOST_AND_FOUND = 9;
/*
* Description: Enables automatic TAK PLI broadcasts and reduces routine broadcasts.
* Technical Details: Turns off many of the routine broadcasts to favor ATAK CoT packet stream
* and automatic TAK PLI (position location information) broadcasts.
* Uses position module configuration to determine TAK PLI broadcast interval.
*/
TAK_TRACKER = 10;
/*
* Description: Will always rebroadcast packets, but will do so after all other modes.
* Technical Details: Used for router nodes that are intended to provide additional coverage
* in areas not already covered by other routers, or to bridge around problematic terrain,
* but should not be given priority over other routers in order to avoid unnecessaraily
* consuming hops.
*/
ROUTER_LATE = 11;
}
/*
* Defines the device's behavior for how messages are rebroadcast
*/
enum RebroadcastMode {
/*
* Default behavior.
* Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params.
*/
ALL = 0;
/*
* Same as behavior as ALL but skips packet decoding and simply rebroadcasts them.
* Only available in Repeater role. Setting this on any other roles will result in ALL behavior.
*/
ALL_SKIP_DECODING = 1;
/*
* Ignores observed messages from foreign meshes that are open or those which it cannot decrypt.
* Only rebroadcasts message on the nodes local primary / secondary channels.
*/
LOCAL_ONLY = 2;
/*
* Ignores observed messages from foreign meshes like LOCAL_ONLY,
* but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB)
*/
KNOWN_ONLY = 3;
}
/*
* Sets the role of node
*/
Role role = 1;
/*
* Disabling this will disable the SerialConsole by not initilizing the StreamAPI
*/
bool serial_enabled = 2;
/*
* By default we turn off logging as soon as an API client connects (to keep shared serial link quiet).
* Set this to true to leave the debug log outputting even when API is active.
*/
bool debug_log_enabled = 3;
/*
* For boards without a hard wired button, this is the pin number that will be used
* Boards that have more than one button can swap the function with this one. defaults to BUTTON_PIN if defined.
*/
uint32 button_gpio = 4;
/*
* For boards without a PWM buzzer, this is the pin number that will be used
* Defaults to PIN_BUZZER if defined.
*/
uint32 buzzer_gpio = 5;
/*
* Sets the role of node
*/
RebroadcastMode rebroadcast_mode = 6;
/*
* Send our nodeinfo this often
* Defaults to 900 Seconds (15 minutes)
*/
uint32 node_info_broadcast_secs = 7;
/*
* Treat double tap interrupt on supported accelerometers as a button press if set to true
*/
bool double_tap_as_button_press = 8;
/*
* If true, device is considered to be "managed" by a mesh administrator
* Clients should then limit available configuration and administrative options inside the user interface
*/
bool is_managed = 9;
/*
* Disables the triple-press of user button to enable or disable GPS
*/
bool disable_triple_click = 10;
}
/*
* Position Config
*/
message PositionConfig {
/*
* Bit field of boolean configuration options, indicating which optional
* fields to include when assembling POSITION messages.
* Longitude, latitude, altitude, speed, heading, and DOP
* are always included (also time if GPS-synced)
* NOTE: the more fields are included, the larger the message will be -
* leading to longer airtime and a higher risk of packet loss
*/
enum PositionFlags {
/*
* Required for compilation
*/
UNSET = 0x0000;
/*
* Include an altitude value (if available)
*/
ALTITUDE = 0x0001;
/*
* Altitude value is MSL
*/
ALTITUDE_MSL = 0x0002;
/*
* Include geoidal separation
*/
GEOIDAL_SEPARATION = 0x0004;
/*
* Include the DOP value ; PDOP used by default, see below
*/
DOP = 0x0008;
/*
* If POS_DOP set, send separate HDOP / VDOP values instead of PDOP
*/
HVDOP = 0x0010;
/*
* Include number of "satellites in view"
*/
SATINVIEW = 0x0020;
/*
* Include a sequence number incremented per packet
*/
SEQ_NO = 0x0040;
/*
* Include positional timestamp (from GPS solution)
*/
TIMESTAMP = 0x0080;
/*
* Include positional heading
* Intended for use with vehicle not walking speeds
* walking speeds are likely to be error prone like the compass
*/
HEADING = 0x0100;
/*
* Include positional speed
* Intended for use with vehicle not walking speeds
* walking speeds are likely to be error prone like the compass
*/
SPEED = 0x0200;
}
enum GpsMode {
/*
* GPS is present but disabled
*/
DISABLED = 0;
/*
* GPS is present and enabled
*/
ENABLED = 1;
/*
* GPS is not present on the device
*/
NOT_PRESENT = 2;
}
/*
* We should send our position this often (but only if it has changed significantly)
* Defaults to 15 minutes
*/
uint32 position_broadcast_secs = 1;
/*
* Adaptive position braoadcast, which is now the default.
*/
bool position_broadcast_smart_enabled = 2;
/*
* If set, this node is at a fixed position.
* We will generate GPS position updates at the regular interval, but use whatever the last lat/lon/alt we have for the node.
* The lat/lon/alt can be set by an internal GPS or with the help of the app.
*/
bool fixed_position = 3;
/*
* Is GPS enabled for this node?
*/
bool gps_enabled = 4[deprecated = true];
/*
* How often should we try to get GPS position (in seconds)
* or zero for the default of once every 30 seconds
* or a very large value (maxint) to update only once at boot.
*/
uint32 gps_update_interval = 5;
/*
* Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time
*/
uint32 gps_attempt_time = 6 [deprecated = true];
/*
* Bit field of boolean configuration options for POSITION messages
* (bitwise OR of PositionFlags)
*/
uint32 position_flags = 7;
/*
* (Re)define GPS_RX_PIN for your board.
*/
uint32 rx_gpio = 8;
/*
* (Re)define GPS_TX_PIN for your board.
*/
uint32 tx_gpio = 9;
/*
* The minimum distance in meters traveled (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
*/
uint32 broadcast_smart_minimum_distance = 10;
/*
* The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
*/
uint32 broadcast_smart_minimum_interval_secs = 11;
/*
* (Re)define PIN_GPS_EN for your board.
*/
uint32 gps_en_gpio = 12;
/*
* Set where GPS is enabled, disabled, or not present
*/
GpsMode gps_mode = 13;
}
/*
* Power Config\
* See [Power Config](/docs/settings/config/power) for additional power config details.
*/
message PowerConfig {
/*
* If set, we are powered from a low-current source (i.e. solar), so even if it looks like we have power flowing in
* we should try to minimize power consumption as much as possible.
* YOU DO NOT NEED TO SET THIS IF YOU'VE set is_router (it is implied in that case).
* Advanced Option
*/
bool is_power_saving = 1;
/*
* If non-zero, the device will fully power off this many seconds after external power is removed.
*/
uint32 on_battery_shutdown_after_secs = 2;
/*
* Ratio of voltage divider for battery pin eg. 3.20 (R1=100k, R2=220k)
* Overrides the ADC_MULTIPLIER defined in variant for battery voltage calculation.
* Should be set to floating point value between 2 and 4
* Fixes issues on Heltec v2
*/
float adc_multiplier_override = 3;
/*
* Wait Bluetooth Seconds
* The number of seconds for to wait before turning off BLE in No Bluetooth states
* 0 for default of 1 minute
*/
uint32 wait_bluetooth_secs = 4;
/*
* Super Deep Sleep Seconds
* While in Light Sleep if mesh_sds_timeout_secs is exceeded we will lower into super deep sleep
* for this value (default 1 year) or a button press
* 0 for default of one year
*/
uint32 sds_secs = 6;
/*
* Light Sleep Seconds
* In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on
* ESP32 Only
* 0 for default of 300
*/
uint32 ls_secs = 7;
/*
* Minimum Wake Seconds
* While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value
* 0 for default of 10 seconds
*/
uint32 min_wake_secs = 8;
/*
* I2C address of INA_2XX to use for reading device battery voltage
*/
uint32 device_battery_ina_address = 9;
}
/*
* Network Config
*/
message NetworkConfig {
enum AddressMode {
/*
* obtain ip address via DHCP
*/
DHCP = 0;
/*
* use static ip address
*/
STATIC = 1;
}
message IpV4Config {
/*
* Static IP address
*/
fixed32 ip = 1;
/*
* Static gateway address
*/
fixed32 gateway = 2;
/*
* Static subnet mask
*/
fixed32 subnet = 3;
/*
* Static DNS server address
*/
fixed32 dns = 4;
}
/*
* Enable WiFi (disables Bluetooth)
*/
bool wifi_enabled = 1;
/*
* If set, this node will try to join the specified wifi network and
* acquire an address via DHCP
*/
string wifi_ssid = 3;
/*
* If set, will be use to authenticate to the named wifi
*/
string wifi_psk = 4;
/*
* NTP server to use if WiFi is conneced, defaults to `0.pool.ntp.org`
*/
string ntp_server = 5;
/*
* Enable Ethernet
*/
bool eth_enabled = 6;
/*
* acquire an address via DHCP or assign static
*/
AddressMode address_mode = 7;
/*
* struct to keep static address
*/
IpV4Config ipv4_config = 8;
/*
* rsyslog Server and Port
*/
string rsyslog_server = 9;
}
/*
* Display Config
*/
message DisplayConfig {
/*
* How the GPS coordinates are displayed on the OLED screen.
*/
enum GpsCoordinateFormat {
/*
* GPS coordinates are displayed in the normal decimal degrees format:
* DD.DDDDDD DDD.DDDDDD
*/
DEC = 0;
/*
* GPS coordinates are displayed in the degrees minutes seconds format:
* DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
*/
DMS = 1;
/*
* Universal Transverse Mercator format:
* ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
*/
UTM = 2;
/*
* Military Grid Reference System format:
* ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
* E is easting, N is northing
*/
MGRS = 3;
/*
* Open Location Code (aka Plus Codes).
*/
OLC = 4;
/*
* Ordnance Survey Grid Reference (the National Grid System of the UK).
* Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
* E is the easting, N is the northing
*/
OSGR = 5;
}
/*
* Unit display preference
*/
enum DisplayUnits {
/*
* Metric (Default)
*/
METRIC = 0;
/*
* Imperial
*/
IMPERIAL = 1;
}
/*
* Override OLED outo detect with this if it fails.
*/
enum OledType {
/*
* Default / Auto
*/
OLED_AUTO = 0;
/*
* Default / Auto
*/
OLED_SSD1306 = 1;
/*
* Default / Auto
*/
OLED_SH1106 = 2;
/*
* Can not be auto detected but set by proto. Used for 128x128 screens
*/
OLED_SH1107 = 3;
}
/*
* Number of seconds the screen stays on after pressing the user button or receiving a message
* 0 for default of one minute MAXUINT for always on
*/
uint32 screen_on_secs = 1;
/*
* How the GPS coordinates are formatted on the OLED screen.
*/
GpsCoordinateFormat gps_format = 2;
/*
* Automatically toggles to the next page on the screen like a carousel, based the specified interval in seconds.
* Potentially useful for devices without user buttons.
*/
uint32 auto_screen_carousel_secs = 3;
/*
* If this is set, the displayed compass will always point north. if unset, the old behaviour
* (top of display is heading direction) is used.
*/
bool compass_north_top = 4;
/*
* Flip screen vertically, for cases that mount the screen upside down
*/
bool flip_screen = 5;
/*
* Perferred display units
*/
DisplayUnits units = 6;
/*
* Override auto-detect in screen
*/
OledType oled = 7;
enum DisplayMode {
/*
* Default. The old style for the 128x64 OLED screen
*/
DEFAULT = 0;
/*
* Rearrange display elements to cater for bicolor OLED displays
*/
TWOCOLOR = 1;
/*
* Same as TwoColor, but with inverted top bar. Not so good for Epaper displays
*/
INVERTED = 2;
/*
* TFT Full Color Displays (not implemented yet)
*/
COLOR = 3;
}
/*
* Display Mode
*/
DisplayMode displaymode = 8;
/*
* Print first line in pseudo-bold? FALSE is original style, TRUE is bold
*/
bool heading_bold = 9;
/*
* Should we wake the screen up on accelerometer detected motion or tap
*/
bool wake_on_tap_or_motion = 10;
}
/*
* Lora Config
*/
message LoRaConfig {
enum RegionCode {
/*
* Region is not set
*/
UNSET = 0;
/*
* United States
*/
US = 1;
/*
* European Union 433mhz
*/
EU_433 = 2;
/*
* European Union 868mhz
*/
EU_868 = 3;
/*
* China
*/
CN = 4;
/*
* Japan
*/
JP = 5;
/*
* Australia / New Zealand
*/
ANZ = 6;
/*
* Korea
*/
KR = 7;
/*
* Taiwan
*/
TW = 8;
/*
* Russia
*/
RU = 9;
/*
* India
*/
IN = 10;
/*
* New Zealand 865mhz
*/
NZ_865 = 11;
/*
* Thailand
*/
TH = 12;
/*
* WLAN Band
*/
LORA_24 = 13;
/*
* Ukraine 433mhz
*/
UA_433 = 14;
/*
* Ukraine 868mhz
*/
UA_868 = 15;
/*
* Malaysia 433mhz
*/
MY_433 = 16;
/*
* Malaysia 919mhz
*/
MY_919 = 17;
/*
* Singapore 923mhz
*/
SG_923 = 18;
/*
* Philippines 433mhz
*/
PH_433 = 19;
/*
* Philippines 868mhz
*/
PH_868 = 20;
/*
* Philippines 915mhz
*/
PH_915 = 21;
}
/*
* Standard predefined channel settings
* Note: these mappings must match ModemPreset Choice in the device code.
*/
enum ModemPreset {
/*
* Long Range - Fast
*/
LONG_FAST = 0;
/*
* Long Range - Slow
*/
LONG_SLOW = 1;
/*
* Very Long Range - Slow
*/
VERY_LONG_SLOW = 2;
/*
* Medium Range - Slow
*/
MEDIUM_SLOW = 3;
/*
* Medium Range - Fast
*/
MEDIUM_FAST = 4;
/*
* Short Range - Slow
*/
SHORT_SLOW = 5;
/*
* Short Range - Fast
*/
SHORT_FAST = 6;
/*
* Long Range - Moderately Fast
*/
LONG_MODERATE = 7;
/*
* Short Range - Turbo
* This is the fastest preset and the only one with 500kHz bandwidth.
* It is not legal to use in all regions due to this wider bandwidth.
*/
SHORT_TURBO = 8;
}
/*
* When enabled, the `modem_preset` fields will be adhered to, else the `bandwidth`/`spread_factor`/`coding_rate`
* will be taked from their respective manually defined fields
*/
bool use_preset = 1;
/*
* Either modem_config or bandwidth/spreading/coding will be specified - NOT BOTH.
* As a heuristic: If bandwidth is specified, do not use modem_config.
* Because protobufs take ZERO space when the value is zero this works out nicely.
* This value is replaced by bandwidth/spread_factor/coding_rate.
* If you'd like to experiment with other options add them to MeshRadio.cpp in the device code.
*/
ModemPreset modem_preset = 2;
/*
* Bandwidth in MHz
* Certain bandwidth numbers are 'special' and will be converted to the
* appropriate floating point value: 31 -> 31.25MHz
*/
uint32 bandwidth = 3;
/*
* A number from 7 to 12.
* Indicates number of chirps per symbol as 1<<spread_factor.
*/
uint32 spread_factor = 4;
/*
* The denominator of the coding rate.
* ie for 4/5, the value is 5. 4/8 the value is 8.
*/
uint32 coding_rate = 5;
/*
* This parameter is for advanced users with advanced test equipment, we do not recommend most users use it.
* A frequency offset that is added to to the calculated band center frequency.
* Used to correct for crystal calibration errors.
*/
float frequency_offset = 6;
/*
* The region code for the radio (US, CN, EU433, etc...)
*/
RegionCode region = 7;
/*
* Maximum number of hops. This can't be greater than 7.
* Default of 3
* Attempting to set a value > 7 results in the default
*/
uint32 hop_limit = 8;
/*
* Disable TX from the LoRa radio. Useful for hot-swapping antennas and other tests.
* Defaults to false
*/
bool tx_enabled = 9;
/*
* If zero, then use default max legal continuous power (ie. something that won't
* burn out the radio hardware)
* In most cases you should use zero here.
* Units are in dBm.
*/
int32 tx_power = 10;
/*
* This controls the actual hardware frequency the radio transmits on.
* Most users should never need to be exposed to this field/concept.
* A channel number between 1 and NUM_CHANNELS (whatever the max is in the current region).
* If ZERO then the rule is "use the old channel name hash based
* algorithm to derive the channel number")
* If using the hash algorithm the channel number will be: hash(channel_name) %
* NUM_CHANNELS (Where num channels depends on the regulatory region).
*/
uint32 channel_num = 11;
/*
* If true, duty cycle limits will be exceeded and thus you're possibly not following
* the local regulations if you're not a HAM.
* Has no effect if the duty cycle of the used region is 100%.
*/
bool override_duty_cycle = 12;
/*
* If true, sets RX boosted gain mode on SX126X based radios
*/
bool sx126x_rx_boosted_gain = 13;
/*
* This parameter is for advanced users and licensed HAM radio operators.
* Ignore Channel Calculation and use this frequency instead. The frequency_offset
* will still be applied. This will allow you to use out-of-band frequencies.
* Please respect your local laws and regulations. If you are a HAM, make sure you
* enable HAM mode and turn off encryption.
*/
float override_frequency = 14;
/*
* For testing it is useful sometimes to force a node to never listen to
* particular other nodes (simulating radio out of range). All nodenums listed
* in ignore_incoming will have packets they send dropped on receive (by router.cpp)
*/
repeated uint32 ignore_incoming = 103;
/*
* If true, the device will not process any packets received via LoRa that passed via MQTT anywhere on the path towards it.
*/
bool ignore_mqtt = 104;
}
message BluetoothConfig {
enum PairingMode {
/*
* Device generates a random PIN that will be shown on the screen of the device for pairing
*/
RANDOM_PIN = 0;
/*
* Device requires a specified fixed PIN for pairing
*/
FIXED_PIN = 1;
/*
* Device requires no PIN for pairing
*/
NO_PIN = 2;
}
/*
* Enable Bluetooth on the device
*/
bool enabled = 1;
/*
* Determines the pairing strategy for the device
*/
PairingMode mode = 2;
/*
* Specified PIN for PairingMode.FixedPin
*/
uint32 fixed_pin = 3;
}
/*
* Payload Variant
*/
oneof payload_variant {
DeviceConfig device = 1;
PositionConfig position = 2;
PowerConfig power = 3;
NetworkConfig network = 4;
DisplayConfig display = 5;
LoRaConfig lora = 6;
BluetoothConfig bluetooth = 7;
}
}

View File

@ -1 +0,0 @@
*WifiConnectionStatus.ssid max_size:33

View File

@ -1,120 +0,0 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ConnStatusProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
message DeviceConnectionStatus {
/*
* WiFi Status
*/
optional WifiConnectionStatus wifi = 1;
/*
* WiFi Status
*/
optional EthernetConnectionStatus ethernet = 2;
/*
* Bluetooth Status
*/
optional BluetoothConnectionStatus bluetooth = 3;
/*
* Serial Status
*/
optional SerialConnectionStatus serial = 4;
}
/*
* WiFi connection status
*/
message WifiConnectionStatus {
/*
* Connection status
*/
NetworkConnectionStatus status = 1;
/*
* WiFi access point SSID
*/
string ssid = 2;
/*
* RSSI of wireless connection
*/
int32 rssi = 3;
}
/*
* Ethernet connection status
*/
message EthernetConnectionStatus {
/*
* Connection status
*/
NetworkConnectionStatus status = 1;
}
/*
* Ethernet or WiFi connection status
*/
message NetworkConnectionStatus {
/*
* IP address of device
*/
fixed32 ip_address = 1;
/*
* Whether the device has an active connection or not
*/
bool is_connected = 2;
/*
* Whether the device has an active connection to an MQTT broker or not
*/
bool is_mqtt_connected = 3;
/*
* Whether the device is actively remote syslogging or not
*/
bool is_syslog_connected = 4;
}
/*
* Bluetooth connection status
*/
message BluetoothConnectionStatus {
/*
* The pairing PIN for bluetooth
*/
uint32 pin = 1;
/*
* RSSI of bluetooth connection
*/
int32 rssi = 2;
/*
* Whether the device has an active connection or not
*/
bool is_connected = 3;
}
/*
* Serial connection status
*/
message SerialConnectionStatus {
/*
* Serial baud rate
*/
uint32 baud = 1;
/*
* Whether the device has an active connection or not
*/
bool is_connected = 2;
}

View File

@ -1,19 +0,0 @@
# options for nanopb
# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
# FIXME pick a higher number someday? or do dynamic alloc in nanopb?
*DeviceState.node_db_lite max_count:100
# FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in RAM
*DeviceState.receive_queue max_count:1
*ChannelFile.channels max_count:8
*OEMStore.oem_text max_size:40
*OEMStore.oem_icon_bits max_size:2048
*OEMStore.oem_aes_key max_size:32
*DeviceState.node_remote_hardware_pins max_count:12
*NodeInfoLite.channel int_size:8
*NodeInfoLite.hops_away int_size:8

Some files were not shown because too many files have changed in this diff Show More