Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions apps/web/public/i18n/locales/en/nodes.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,11 @@
"ignoreNode": "Ignore Node",
"unignoreNode": "Unignore Node",
"requestPosition": "Request Position"
},
"signalQuality": {
"good": "Good",
"fair": "Fair",
"bad": "Weak",
"none": "No signal"
}
}
61 changes: 50 additions & 11 deletions apps/web/src/components/PageComponents/Map/Layers/SNRLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ function makeFeature(
toPos: LngLat,
snr: number,
curved: boolean,
preset?: number | string | null,
): Feature | undefined {
const segment = arcSegment(fromPos, toPos, curved);

Expand All @@ -157,7 +158,7 @@ function makeFeature(
type: "Feature",
geometry: { type: "LineString", coordinates: segment },
properties: {
color: getSignalColor(snr),
color: getSignalColor(snr, undefined, preset),
snr,
from: fromId,
to: toId,
Expand All @@ -173,15 +174,17 @@ function pushIfFeature(
snr: number,
curved: boolean,
features: Feature[],
preset?: number | string | null,
) {
const feat = makeFeature(a, b, aPos, bPos, snr, curved);
const feat = makeFeature(a, b, aPos, bPos, snr, curved, preset);
if (feat) {
features.push(feat);
}
}

function generateNeighborLines(
neighborInfos: NeighborInfos[],
preset?: number | string | null,
): FeatureCollection {
// Collect positions for all referenced nodes, discard pairs with missing positions
const idToLngLat = new Map<number, LngLat>();
Expand Down Expand Up @@ -230,15 +233,51 @@ function generateNeighborLines(

if (pair.ab && pair.ba) {
// both directions → two arcs
pushIfFeature(pair.a, pair.b, aPos, bPos, pair.ab, true, features);
pushIfFeature(pair.b, pair.a, bPos, aPos, pair.ba, true, features);
pushIfFeature(
pair.a,
pair.b,
aPos,
bPos,
pair.ab,
true,
features,
preset,
);
pushIfFeature(
pair.b,
pair.a,
bPos,
aPos,
pair.ba,
true,
features,
preset,
);
} else {
// only one direction → straight
if (pair.ab) {
pushIfFeature(pair.a, pair.b, aPos, bPos, pair.ab, false, features);
pushIfFeature(
pair.a,
pair.b,
aPos,
bPos,
pair.ab,
false,
features,
preset,
);
}
if (pair.ba) {
pushIfFeature(pair.b, pair.a, bPos, aPos, pair.ba, false, features);
pushIfFeature(
pair.b,
pair.a,
bPos,
aPos,
pair.ba,
false,
features,
preset,
);
}
}
}
Expand Down Expand Up @@ -288,7 +327,7 @@ export const SNRLayer = ({
myNode,
visibilityState,
}: SNRLayerProps): React.ReactNode => {
const { getNeighborInfo } = useDevice();
const { getNeighborInfo, config } = useDevice();

const remotePairs = visibilityState.remoteNeighbors
? filteredNodes.flatMap((node) => {
Expand Down Expand Up @@ -325,10 +364,10 @@ export const SNRLayer = ({
}))
: [];

const featureCollection = generateNeighborLines([
...remotePairs,
...directPairs,
]);
const featureCollection = generateNeighborLines(
[...remotePairs, ...directPairs],
config.lora?.modemPreset,
);

return (
<Source type="geojson" data={featureCollection}>
Expand Down
37 changes: 26 additions & 11 deletions apps/web/src/components/PageComponents/Map/Popups/NodeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Separator } from "@components/UI/Separator.tsx";
import { Heading } from "@components/UI/Typography/Heading.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { formatQuantity } from "@core/utils/string.ts";
import { rateSignalQuality } from "@core/utils/signalQuality.ts";
import { useDevice } from "@core/stores";
import type { Protobuf as ProtobufType } from "@meshtastic/sdk";
import { Protobuf } from "@meshtastic/sdk";
import {
Expand All @@ -17,11 +19,14 @@ import {
} from "@radix-ui/react-tooltip";
import { useNavigate } from "@tanstack/react-router";
import {
Dot,
LockIcon,
LockOpenIcon,
MessageSquareIcon,
MountainSnow,
Signal,
SignalHigh,
SignalLow,
SignalMedium,
Star,
} from "lucide-react";
import { useTranslation } from "react-i18next";
Expand All @@ -32,6 +37,7 @@ export interface NodeDetailProps {

export const NodeDetail = ({ node }: NodeDetailProps) => {
const navigate = useNavigate();
const { config } = useDevice();
const { t } = useTranslation("nodes");
const name = node.user?.longName ?? t("unknown.shortName");
const shortName = node.user?.shortName ?? t("unknown.shortName");
Expand All @@ -44,17 +50,26 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
? t("unset")
: rawHardwareType.replaceAll("_", " ")
: `${hwModel}`;
// SNR is in dB; derive a 0–100% quality heuristic (-10 dB → 0%, +10 dB → 100%).
const snrQuality = Math.min(
Math.max(Math.round((node.snr + 10) * 5), 0),
100,
);
// SNR rated against the active modem preset's demodulation floor
// (meshtastic/web#1241). WCAG 1.4.1: tier encoded in color + icon + text.
const quality =
node.snr == null
? undefined
: rateSignalQuality(node.snr, config.lora?.modemPreset);
const snrTone =
snrQuality >= 67
quality === "good"
? "text-green-600"
: snrQuality >= 34
: quality === "fair"
? "text-yellow-600"
: "text-red-600";
const SignalIcon =
quality === "good"
? SignalHigh
: quality === "fair"
? SignalMedium
: quality === "bad"
? SignalLow
: Signal;
function handleDirectMessage() {
navigate({ to: `/messages/direct/${node.num}` });
}
Expand Down Expand Up @@ -208,15 +223,15 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
)}
</div>

{node.snr !== 0 && (
{node.snr != null && quality && (
<div className="mt-2">
<div>{t("unit.snr")}</div>
<Mono className="flex items-center gap-1 text-xs">
<SignalIcon size={14} className={snrTone} aria-hidden="true" />
<span className={snrTone}>{t(`signalQuality.${quality}`)}</span>
<span className={snrTone}>
{Number(node.snr.toFixed(1))} {t("unit.db")}
</span>
<Dot className="text-gray-400" />
<span className="text-gray-500">{snrQuality}%</span>
</Mono>
</div>
)}
Expand Down
33 changes: 11 additions & 22 deletions apps/web/src/core/utils/signalColor.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,20 @@
export const SNR_THRESHOLD = {
GOOD: -7,
FAIR: -15,
};

export const RSSI_THRESHOLD = {
GOOD: -115,
FAIR: -126,
};
import { rateSignalQuality } from "./signalQuality.ts";

export const LINE_COLOR = {
GOOD: "#00ff00",
FAIR: "#ffe600",
BAD: "#f7931a",
};

export const getSignalColor = (snr: number, rssi?: number): string => {
if (
snr > SNR_THRESHOLD.GOOD &&
(rssi == null || rssi > RSSI_THRESHOLD.GOOD)
) {
return LINE_COLOR.GOOD;
}
if (
snr > SNR_THRESHOLD.FAIR &&
(rssi == null || rssi > RSSI_THRESHOLD.FAIR)
) {
return LINE_COLOR.FAIR;
}
export const getSignalColor = (
snr: number,
rssi?: number,
preset?: number | string | null,
): string => {
// Preset-relative quality per meshtastic/web#1241; "none" (no chance of
// demodulation) renders as BAD on the map.
const quality = rateSignalQuality(snr, preset, rssi);
if (quality === "good") return LINE_COLOR.GOOD;
if (quality === "fair") return LINE_COLOR.FAIR;
return LINE_COLOR.BAD;
};
Loading