Early drafts of web tracker, not complete

This commit is contained in:
John Poole 2026-02-20 17:43:00 -08:00
commit 67e1a78995
2 changed files with 212 additions and 0 deletions

View file

@ -0,0 +1,157 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Fieldtest Positions</title>
<!-- Leaflet (swap to your local staged copies if preferred)
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> -->
<link rel="stylesheet" href="https://ryzdesk/lib/leaflet.css">
<script src="https://ryzdesk/lib/leaflet.js"></script>
<style>
html, body { height: 100%; margin: 0; }
#map { height: 100%; width: 100%; }
.legend {
position: absolute; z-index: 1000; background: rgba(255,255,255,0.85);
padding: 8px; margin: 10px; border-radius: 6px; font-family: sans-serif;
font-size: 13px;
}
.node-label {
background: rgba(255,255,255,0.85);
border: 1px solid rgba(0,0,0,0.25);
color: #000;
font-weight: 700;
padding: 1px 6px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.25);
}
</style>
</head>
<body>
<div class="legend">
WS: <span id="ws_state">connecting</span><br>
Last: <span id="last_msg"></span>
</div>
<div id="map"></div>
<script>
// --- Map init (center near your sample points; adjust as desired)
const map = L.map('map').setView([44.93655, -123.02185], 17);
// Basemap: replace with your own tile server if you have one
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 20,
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
// Node colors (bright, readable)
const nodeStyle = {
B: { color: '#00e5ff' }, // cyan-ish
C: { color: '#ffea00' }, // yellow
D: { color: '#ff3d00' }, // orange/red
E: { color: '#76ff03' }, // green
};
// One persistent "current" marker per node (bright + labeled)
const currentMarkers = {}; // node -> L.CircleMarker
function addFadingDot(node, lat, lon) {
const style = nodeStyle[node] || { color: '#ffffff' };
// Bright at birth
const m = L.circleMarker([lat, lon], {
radius: 7,
color: style.color,
weight: 2,
fillColor: style.color,
fillOpacity: 1.0,
opacity: 1.0
}).addTo(map);
// Fade steps: 5s -> 2/3, 10s -> 1/3, 15s -> remove
setTimeout(() => {
m.setStyle({ fillOpacity: 0.66, opacity: 0.66 });
}, 5000);
setTimeout(() => {
m.setStyle({ fillOpacity: 0.33, opacity: 0.33 });
}, 10000);
setTimeout(() => {
map.removeLayer(m);
}, 15000);
}
function upsertCurrentMarker(node, lat, lon) {
const style = nodeStyle[node] || { color: '#ffffff' };
if (!currentMarkers[node]) {
const m = L.circleMarker([lat, lon], {
radius: 9,
color: style.color,
weight: 2,
fillColor: style.color,
fillOpacity: 1.0,
opacity: 1.0
}).addTo(map);
// Permanent label next to the current marker
m.bindTooltip(node, {
permanent: true,
direction: 'right',
offset: [10, 0],
className: 'node-label'
});
currentMarkers[node] = m;
} else {
currentMarkers[node].setLatLng([lat, lon]);
// reassert brightness in case you later add effects
currentMarkers[node].setStyle({ fillOpacity: 1.0, opacity: 1.0 });
}
}
function parseMsg(line) {
// "C,44.936454,-123.021923,65.40"
const parts = line.trim().split(',');
if (parts.length < 4) return null;
const node = parts[0];
const lat = parseFloat(parts[1]);
const lon = parseFloat(parts[2]);
const alt = parseFloat(parts[3]); // not used yet, but keep it
if (!node || Number.isNaN(lat) || Number.isNaN(lon)) return null;
return { node, lat, lon, alt };
}
// --- WebSocket
const wsStateEl = document.getElementById('ws_state');
const lastMsgEl = document.getElementById('last_msg');
//const wsUrl = `ws://${location.hostname}:3000/stream`;
//const ws = new WebSocket(wsUrl);
const wsScheme = (location.protocol === 'https:') ? 'wss' : 'ws';
const wsUrl = (wsScheme === 'ws')
? `ws://${location.hostname}:3000/stream`
: `wss://${location.host}/stream`;
const ws = new WebSocket(wsUrl);
//const ws = new WebSocket('ws://localhost:3000/stream'); // temporary test
ws.onopen = () => wsStateEl.textContent = 'open';
ws.onclose = () => wsStateEl.textContent = 'closed';
ws.onerror = () => wsStateEl.textContent = 'error';
ws.onmessage = (ev) => {
const line = ev.data;
lastMsgEl.textContent = line;
const msg = parseMsg(line);
if (!msg) return;
addFadingDot(msg.node, msg.lat, msg.lon);
upsertCurrentMarker(msg.node, msg.lat, msg.lon);
};
</script>
</body>
</html>

View file

@ -0,0 +1,55 @@
#!/usr/bin/env perl
# 20260219 ChatGPT
# $Id$
# $HeadURL$
# EXAMPLE:
# perl fwd_positions_udp.pl \
# --file /usr/local/src/sx1302_hal/util_net_downlink/uplinks_20260219_123102.csv \
# --host ryzdesk --port 1777
use strict;
use warnings;
use IO::Socket::INET;
use Getopt::Long qw(GetOptions);
my ($file, $host, $port) = ("", "ryzdesk", 1777);
GetOptions(
"file=s" => \$file,
"host=s" => \$host,
"port=i" => \$port,
) or die "bad args\n";
die "--file required\n" if !$file;
my $sock = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Proto => "udp",
) or die "udp socket: $!\n";
£ Use tail -F so rotations keep working
open(my $fh, "-|", "tail", "-F", $file) or die "tail: $!\n";
$| = 1;
while (my $line = <$fh>) {
chomp $line;
£ Your CSV has the hex payload in field 16 (1-based) => index 15 (0-based)
my @f = split(/,/, $line, -1);
next if @f < 16;
my $hex = $f[15] // "";
$hex =~ s/[^0-9A-Fa-f]//g;
next if $hex eq "";
my $msg = pack("H*", $hex); £ yields: "C,44.936454,-123.021923,65.40"
$msg =~ s/\r?\n$//;
£ sanity filter: must look like: X,lat,lon,alt
next unless $msg =~ /^[A-Z],[+-]?\d+\.\d+,[+-]?\d+\.\d+,[+-]?\d+(\.\d+)?$/;
$sock->send($msg."\n");
print "$msg\n"; £ local visibility
}