Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1ca87be
Step 1,2
TheWISPRer May 30, 2025
4db50dd
Basic implementation maybe?
TheWISPRer May 30, 2025
30a2905
Fix broken Util in Update.mjs (styleText replaced with chalk)
TheWISPRer May 30, 2025
f7fa87a
Fix compile issues with maptrails
TheWISPRer May 30, 2025
4776530
Add crypto import to API index.ts
TheWISPRer May 30, 2025
c1765c7
Undid previous change
TheWISPRer May 30, 2025
ccc63bc
Connect button not working. Testing changes
TheWISPRer May 30, 2025
a787142
Something something Node bullshit with the crypto module
TheWISPRer May 30, 2025
a3ffac1
It ran before this, lets find out if it still does (Maybe fix trail l…
TheWISPRer Jun 1, 2025
723fb38
Oops. Added an exta "."
TheWISPRer Jun 1, 2025
ee2ab3d
Added historical trail resources
TheWISPRer Jun 1, 2025
3826141
OOps. Forgot a file
TheWISPRer Jun 1, 2025
3fa1c61
Fixes or something
TheWISPRer Jun 2, 2025
73bf954
Windows compile fixes with Electron
TheWISPRer Jun 2, 2025
9a196cf
Still having issues on windows compile
TheWISPRer Jun 2, 2025
309237c
Node 18 remove styletext
TheWISPRer Jun 2, 2025
4671b8c
more styletext
TheWISPRer Jun 2, 2025
cea322e
hate
TheWISPRer Jun 2, 2025
95a0060
Pain
TheWISPRer Jun 2, 2025
8ae0a4c
Maybe lines show up now, added some debugging
TheWISPRer Jun 2, 2025
37ae7ea
Add some ui elements to show trail history
TheWISPRer Jun 2, 2025
a9bda15
Merge remote-tracking branch 'MeshSense_Affirmatech/master' into MapT…
TheWISPRer Jun 2, 2025
ef328f0
Added some buttons hopefully
TheWISPRer Jun 2, 2025
c2ce4fc
Forgot the buttons
TheWISPRer Jun 2, 2025
dc21a29
Toooo many buttons now
TheWISPRer Jun 2, 2025
f3f675c
Something is working, but nothing is working
TheWISPRer Jun 2, 2025
1709095
*groans*
TheWISPRer Jun 2, 2025
1983149
Fixed delete nodes sendstatus missing
TheWISPRer Jun 3, 2025
a925af9
Some fixes hopefully
TheWISPRer Jun 3, 2025
d9cbb4d
Fixed coodinate conversions
TheWISPRer Jun 4, 2025
ce886ea
Something is working
TheWISPRer Jun 4, 2025
a6bb2cc
Updated trail ui elements
TheWISPRer Jun 4, 2025
306ae52
Forgot a comma
TheWISPRer Jun 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ export default defineConfig({
ignoreGlobal: true
}),
typescript({
target: 'esnext'
target: 'esnext',
// sourceMap: true
exclude: ['webbluetooth/build/**']
}),
json()
]
Expand Down
22 changes: 22 additions & 0 deletions api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import { webcrypto } from 'crypto';

declare global {
// Let TypeScript know about globalThis.crypto
// eslint-disable-next-line no-var
var crypto: typeof webcrypto;
}

globalThis.crypto = webcrypto;

import 'dotenv/config'
import './lib/persistence'
import { app, createRoutes, finalize, server } from './lib/server'
Expand All @@ -10,6 +20,8 @@ import { createWriteStream } from 'fs'
import { dataDirectory } from './lib/paths'
import { join } from 'path'
import axios from 'axios'
import express from 'express';
import { nodeHistoryMap } from './nodeHistoryStore';
setInterval(() => currentTime.set(Date.now()), 15000)

process.on('uncaughtException', (err, origin) => {
Expand Down Expand Up @@ -72,6 +84,7 @@ createRoutes((app) => {
if (!isAuthorized(req)) return res.sendStatus(403)
let nodes = req.body.nodes
await deleteNodes(nodes)
return res.sendStatus(200)
})

app.post('/connect', async (req, res) => {
Expand Down Expand Up @@ -105,6 +118,15 @@ createRoutes((app) => {
return res.sendStatus(200)
})

app.get('/api/nodes/:nodeNum/history', (req, res) => {
const nodeNum = Number(req.params.nodeNum); // Ensure this is a number
if (isNaN(nodeNum)) {
return res.status(400).json({ error: 'Invalid nodeNum' });
}
const history = nodeHistoryMap.get(nodeNum) || [];
return res.json(history);
});

//** Set accessKey via environment variable */
if (process.env.ACCESS_KEY) {
accessKey.set(process.env.ACCESS_KEY)
Expand Down
19 changes: 17 additions & 2 deletions api/src/meshtastic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import exitHook from 'exit-hook'
import * as geolib from 'geolib'
import axios from 'axios'
import { State } from './lib/state'
import { nodeHistoryMap } from './nodeHistoryStore'

let routeCache: State<Record<number, number[]>>

Expand Down Expand Up @@ -350,8 +351,22 @@ export async function connect(address?: string) {
let packet: MeshPacket
if (id && data.latitudeI) packet = packets.upsert({ id, data })
if (e.from && data.latitudeI) {
let node = nodes.upsert({ num: e.from, position: data })
if (packet?.viaMqtt === false) sendToMeshMap({ num: e.from, position: data }, node, packet)
let nodeNum = Number(e.from)
const record = {
latitudeI: data.latitudeI,
longitudeI: data.longitudeI,
timestampMs: Date.now()
}
if (!nodeHistoryMap.has(nodeNum)) {
nodeHistoryMap.set(nodeNum, [])
}
nodeHistoryMap.get(nodeNum)!.push(record)
// Optionally limit history size:
// const arr = nodeHistoryMap.get(nodeNum)!
// if (arr.length > 1000) arr.shift()
console.log(`Pushed history for node ${nodeNum}:`, record)
let node = nodes.upsert({ num: nodeNum, position: data })
if (packet?.viaMqtt === false) sendToMeshMap({ num: nodeNum, position: data }, node, packet)
}
})

Expand Down
7 changes: 7 additions & 0 deletions api/src/nodeHistoryStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const nodeHistoryMap = new Map<number, Array<{ latitudeI: number, longitudeI: number, timestampMs: number }>>();

// Example: Add a fake history for node 123
nodeHistoryMap.set(123, [
{ latitudeI: 404123456, longitudeI: -747654321, timestampMs: 1620000000000 },
{ latitudeI: 404223456, longitudeI: -747654123, timestampMs: 1620000050000 },
]);
6 changes: 5 additions & 1 deletion api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,9 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
},
"exclude": [
"node_modules",
"webbluetooth/build"
]
}
11 changes: 6 additions & 5 deletions build.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env node
import { spawn } from 'child_process'
import { styleText } from 'util';
import './api/node_modules/dotenv/config.js'

let runCmd = (commandString) => new Promise((resolve, reject) => {
Expand All @@ -13,14 +12,16 @@ let runCmd = (commandString) => new Promise((resolve, reject) => {

let platform = process.platform.replace(/32$/, '').replace('darwin', 'mac')

console.log(styleText(['magenta', 'bold'], 'Building UI'))
console.log('🏗 Building UI…')
process.chdir('ui')
await runCmd('npm run build')

console.log(styleText(['magenta', 'bold'], 'Building API'))
console.log('🏗 Building API…')
process.chdir('../api')
await runCmd('npm run build')

console.log(styleText(['magenta', 'bold'], 'Building Electron'))
console.log('🏗 Building Electron…')
process.chdir('../electron')
await runCmd(`npm run build:${platform} --c.extraMetadata.version=2.0.0`)
await runCmd(`npm run build:${platform} --c.extraMetadata.version=2.0.0`)

console.log('✅ Build complete!')
9 changes: 9 additions & 0 deletions electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,14 @@
"prettier": "^3.3.3",
"typescript": "^5.6.3",
"vite": "^5.4.9"
},
"build": {
"extraResources": [
{
"from": "../api/dist",
"to": "resources/api",
"filter": ["**/*"]
}
]
}
}
11 changes: 10 additions & 1 deletion electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,17 @@ function createWindow(): void {
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
console.log(`DIRNAME`, __dirname)
let apiPath = join(__dirname, '../../resources/api/index.cjs').replace('app.asar', 'app.asar.unpacked')
// Always resolve API_PATH from process.resourcesPath for both dev and packaged
const apiPath = join(process.resourcesPath, 'api', 'index.cjs')
console.log(`API_PATH`, apiPath)
createWindow()

// Only run auto-update check if we actually have an update manifest
try {
await autoUpdater.checkForUpdatesAndNotify()
} catch (e) {
console.warn('AutoUpdater: no update manifest found, skipping. ', e)
}

apiProcess = utilityProcess.fork(apiPath, process.argv, { stdio: 'pipe' })
apiProcess.stdout?.on('data', (e) => process.stdout.write(e))
Expand Down
127 changes: 125 additions & 2 deletions ui/src/Map.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
import { getCoordinates, getNodeById, getNodeName, getNodeNameById, setPosition } from './lib/util'
import { showConfigModal, showPage } from './SettingsModal.svelte'
import { newsVisible } from './News.svelte'
import { fromLonLat } from 'ol/proj'
import { getNodeHistory, type HistoryRecord } from './stores/nodes';
import { selectedHistoryNode, showHistoryPanel } from './stores/ui';

export let ol: OpenLayersMap = undefined
export let ol: any; // or use the correct type if you have one

$: nodesWithCoords = $filteredNodes.filter((n) => !(n.position?.latitudeI == undefined || n.position?.latitudeI == 0) || n.approximatePosition)

Expand Down Expand Up @@ -68,6 +71,103 @@
}

let modalPage = 'Settings'

let trailArray: { coords: [number, number]; ts: number }[] = [];
let pendingTrail = false;
let timeWindowMs = 6 * 3600 * 1000; // default 6 hours
let rangeStart: number | null = null;
let rangeEnd: number | null = null;

function pruneOldPoints() {
const cutoff = Date.now() - timeWindowMs;
console.log('→ Pruning old points; cutoff =', new Date(cutoff).toLocaleString());
console.log(' before prune:', trailArray.length);
trailArray = trailArray.filter(p => p.ts >= cutoff);
console.log(' after prune:', trailArray.length);
}

function scheduleTrailUpdate() {
if (pendingTrail) return;
pendingTrail = true;

requestAnimationFrame(() => {
const coords = trailArray.map(p => p.coords);
console.log('→ scheduleTrailUpdate called, trailArray length =', trailArray.length);
console.log(' coords:', coords);
ol?.plotTrail(coords);
// also drop timestamped markers along the trail
ol?.plotTrailMarkers(trailArray);
pendingTrail = false;
});
}

$: if (ol) {
ol.plotTrail([])
ol.plotTrailMarkers([])
// ...existing plotData() or other init calls...
}

$: {
// Whenever the selected node or its coords change…
const selectedNode = nodesWithCoords.find(n => n.num === $myNodeNum)
if (selectedNode?.position?.latitudeI && selectedNode?.position?.longitudeI) {
const lon = selectedNode.position.longitudeI / 1e7
const lat = selectedNode.position.latitudeI / 1e7

// Only push if it’s truly new
const last = trailArray[trailArray.length - 1]
if (!last || last.coords[0] !== lon || last.coords[1] !== lat) {
trailArray.push({ coords: [lon, lat], ts: Date.now() })
pruneOldPoints()
scheduleTrailUpdate()
}
}
}

async function onTimestampClick(entry: any) {
if (rangeStart === null || (rangeStart !== null && rangeEnd !== null)) {
rangeStart = entry.timestampMs;
rangeEnd = null;
trailArray = [];
scheduleTrailUpdate();
} else if (rangeEnd === null) {
rangeEnd = entry.timestampMs;
applyHistoryRange();
}
}

async function applyHistoryRange() {
if (!$selectedHistoryNode || rangeStart === null || rangeEnd === null) return;
const historyRecords: HistoryRecord[] = await getNodeHistory($selectedHistoryNode);
const start = Math.min(rangeStart, rangeEnd);
const end = Math.max(rangeStart, rangeEnd);
const points = historyRecords
.map(r => ({
coords: [r.longitudeI / 1e7, r.latitudeI / 1e7] as [number, number],
ts: r.timestampMs
}))
.filter(p => p.ts >= start && p.ts <= end)
.sort((a, b) => a.ts - b.ts);
const uniquePoints = points.filter((p, i, arr) => {
if (i === 0) return true;
const [prevLon, prevLat] = arr[i - 1].coords;
return p.coords[0] !== prevLon || p.coords[1] !== prevLat;
});
trailArray = uniquePoints;
scheduleTrailUpdate();
}

function resetHistoryRange() {
rangeStart = rangeEnd = null;
trailArray = [];
scheduleTrailUpdate();
}

let historyList: any[] = [];

$: if ($selectedHistoryNode && $showHistoryPanel) {
getNodeHistory($selectedHistoryNode).then(list => historyList = list);
}
</script>

<Card title="Map" {...$$restProps}>
Expand Down Expand Up @@ -101,11 +201,34 @@
}
}}
onDarkModeToggle={plotData}
></OpenLayersMap>
/>
{#if $setPositionMode}
<div class="absolute select-none top-10 left-10 bg-indigo-600/80 text-white p-3 py-1 rounded-lg">
Click on a new position for {getNodeNameById($myNodeNum)}
<button title="Cancel selecting a position" class="btn btn-sm ml-2 font-bold !text-red-200 !from-rose-500 !to-rose-800 rounded-full" on:click={() => ($setPositionMode = false)}>X</button>
</div>
{/if}

{#if $showHistoryPanel}
<section class="node-history">
<h3>Node History for #{$selectedHistoryNode}</h3>
<p class="text-sm mb-2">Select start and end timestamps to draw a trail.</p>
{#if historyList.length === 0}
<p><em>No history available for this node.</em></p>
{:else}
{#each historyList as entry (entry.timestampMs)}
<button
type="button"
class="timestamp-item {rangeStart === entry.timestampMs || rangeEnd === entry.timestampMs ? 'bg-indigo-500 text-white' : ''}"
on:click={() => onTimestampClick(entry)}>
{new Date(entry.timestampMs).toLocaleString()}
</button>
{/each}
{/if}
<div class="mt-2 flex gap-2">
<button class="btn" on:click={resetHistoryRange}>Clear Selection</button>
<button class="btn" on:click={() => { resetHistoryRange(); showHistoryPanel.set(false); }}>Close</button>
</div>
</section>
{/if}
</Card>
44 changes: 13 additions & 31 deletions ui/src/Nodes.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import { getSvgUri, setPositionMode } from './Map.svelte'
import ChannelUtilization from './lib/ChannelUtilization.svelte'
import ObservedRF from './lib/ObservedRF.svelte'
import { selectedHistoryNode, showHistoryPanel } from './stores/ui';

export let includeMqtt = (localStorage.getItem('includeMqtt') ?? 'true') == 'true'
let selectedNode: NodeInfo
Expand Down Expand Up @@ -269,7 +270,7 @@
{/if}
<div
class="h-0.5 {getBatteryColor(node.deviceMetrics?.batteryLevel)}"
style="width: {node.deviceMetrics?.batteryLevel || 0}%;
style="width: {node.deviceMetrics?.batteryLevel || 0}% ;
background-color: {node.deviceMetrics?.batteryLevel === 101 ? 'steelblue' : ''};"
></div>
</div>
Expand All @@ -288,6 +289,17 @@
title="Traceroute {node.hopsAway == 0 ? 'Direct' : ''}{node?.trace ? [$myNodeNum, ...node?.trace?.route, node?.num].map((id) => getNodeNameById(id)).join(' -> ') : ''}"
on:click={() => axios.post('/traceRoute', { destination: node.num })}>↯</button
>

<!-- History button-->
<button
class="btn btn-xs"
title="Show Node History"
on:click={() => {
selectedHistoryNode.set(node.num);
showHistoryPanel.set(true);
}}>
H
</button>
{:else if $hasAccess}
<button title="Set Position" class="rounded-md fill-cyan-400/80 text-lg -mx-0.5" on:click={() => ($setPositionMode = true)}
><svg width="24px" height="24px" viewBox="0 0 512 512" data-name="Layer 1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"
Expand Down Expand Up @@ -326,36 +338,6 @@
</button>
{/if}
</div>

{#if node.environmentMetrics}
<div class="flex gap-1">
{#if node.environmentMetrics.temperature}
<div title="Temperature" class="text-sm font-normal bg-purple-950/20 text-purple-200/90 rounded p-0.5 w-12 h-6 text-center overflow-hidden">
{formatTemp(node.environmentMetrics.temperature, $displayFahrenheit)}
</div>
{/if}
{#if node.environmentMetrics.barometricPressure}
<div title="Barometric Pressure" class="text-sm font-normal bg-purple-950/20 text-purple-200/90 rounded p-0.5 w-20 h-6 text-center overflow-hidden">
{Math.round(node.environmentMetrics.barometricPressure)} hPA
</div>
{/if}
{#if node.environmentMetrics.relativeHumidity}
<div title="Relative Humidity" class="text-sm font-normal bg-purple-950/20 text-purple-200/90 rounded p-0.5 w-12 h-6 text-center overflow-hidden">
{Math.round(node.environmentMetrics.relativeHumidity)}%
</div>
{/if}
{#if node.environmentMetrics.gasResistance}
<div title="Gas Resistance" class="text-sm font-normal bg-purple-950/20 text-purple-200/90 rounded p-0.5 w-20 h-6 text-center overflow-hidden">
{Math.round(node.environmentMetrics.gasResistance)} MOhm
</div>
{/if}
{#if node.environmentMetrics.iaq}
<div title="Air Quality" class="text-sm font-normal bg-purple-950/20 text-purple-200/90 rounded p-0.5 w-16 h-6 text-center overflow-hidden">
{Math.round(node.environmentMetrics.iaq)} IAQ
</div>
{/if}
</div>
{/if}
{/if}
</div>
{/each}
Expand Down
Loading