feat(protobufs): sync to firmware-current and consume workspace package (#1097)
* feat(protobufs): sync to firmware-current and consume workspace package Sync the vendored .proto sources to firmware-current (v2.7.25+48), regenerate the v2 TS bindings, and consume the workspace @meshtastic/protobufs (workspace:*) in place of the stale JSR 2.7.20 — finishing the monorepo migration (core was already workspace:*). Includes the one required breaking-change fix: admin nodedb_reset changed int32 to bool, so resetNodes() now sends value: true. * build(protobufs): vendor generated bindings for workspace consumers The package is consumed via workspace:* — its exports point at the TS source, which imports ./dist/meshtastic/*_pb.ts — so the generated output must exist at build time. CI builds web/core with no codegen step and the runners have no buf CLI, so the bindings are vendored here (kept gitignored; lint/format skip them). Regenerate with: pnpm --filter @meshtastic/protobufs gen * fix(protobufs): clean script removes the actual generated output dir buf writes bindings to packages/ts/dist, but clean was removing a non-existent root dist — so it never cleaned stale output. Addresses Copilot review feedback. * refactor: move web app packages/web -> apps/web Aligns the web app with the apps/web layout (matching the Vercel web-test Root Directory and the SDK-migration direction). Pure directory move plus root config: pnpm-workspace (adds apps/*), vitest projects, root tsconfig reference, and the pr/release-web/nightly workflows. vercel.json moved with the app. Build + 36 validation tests green. * feat: config fields, module pages, key verification, telemetry capture Incorporates the firmware-current feature work onto the protobuf foundation: new config fields (Display message bubbles; LoRa fem_lna_mode + serial_hal_only; Telemetry air_quality_screen_enabled); 4 new ModuleConfig pages (TrafficManagement, StatusMessage, TAK, RemoteHardware); the manual Key Verification flow (sendKeyVerification + ClientNotificationDialog stages + Verify Key button); live telemetry capture (nodeDB addDeviceMetrics) and admin hardening (toggleMutedNode, graceful PortNum default); plus the sdk-preview ConfigEditor demo and store/config tests. Build + lint + format + 131 tests green. * chore: drop #1062 (unsaved-change-detection) to match upstream revert #1062 was merged to main by accident (per @danditomaso) and is being reverted. Reverse-applied its diff here via 3-way so #1097 stays consistent with where main is headed, while keeping the feature changes layered on the same files (deviceStore/changeRegistry). Build + 131 tests + lint + format green. * fix(nodes): clean up SNR display in node table and map popup SNR is a ratio measured in dB, not dBm (which is absolute power); the node table and map popup both mislabeled it and crammed three values together: '0dBm/50%/50raw'. The trailing '%/raw' pair was the same heuristic ((snr+10)*5) shown twice — once clamped, once not. Render SNR in dB rounded to one decimal, color-coded by a 0-100% signal-quality heuristic (green/yellow/red), with the quality percentage as a muted secondary. Drop the redundant raw value. Adds unit.db; this matches the existing SNRTooltip, which already renders dB.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
fanOutOffsetsPx,
|
||||
groupNodesByIdenticalCoords,
|
||||
type PxOffset,
|
||||
} from "@components/PageComponents/Map/cluster.ts";
|
||||
import {
|
||||
generatePrecisionCircles,
|
||||
SourcePrecisionCircles,
|
||||
} from "@components/PageComponents/Map/Layers/PrecisionLayer.tsx";
|
||||
import { NodeMarker } from "@components/PageComponents/Map/Markers/NodeMarker.tsx";
|
||||
import { StackBadge } from "@components/PageComponents/Map/Markers/StackBadge.tsx";
|
||||
import { NodeDetail } from "@components/PageComponents/Map/Popups/NodeDetail.tsx";
|
||||
import type { PopupState } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx";
|
||||
import { PopupWrapper } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx";
|
||||
import { useMapFitting } from "@core/hooks/useMapFitting";
|
||||
import { useNodeDB } from "@core/stores";
|
||||
import { hasPos, toLngLat } from "@core/utils/geo.ts";
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { MapRef } from "react-map-gl/maplibre";
|
||||
|
||||
export interface NodeMarkerProps {
|
||||
mapRef: MapRef | undefined;
|
||||
filteredNodes: Protobuf.Mesh.NodeInfo[];
|
||||
myNode: Protobuf.Mesh.NodeInfo | undefined;
|
||||
expandedCluster: string | undefined;
|
||||
setExpandedCluster: (key: string | undefined) => void;
|
||||
popupState: PopupState | undefined;
|
||||
setPopupState: (state: PopupState | undefined) => void;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export const NodesLayer = ({
|
||||
mapRef,
|
||||
filteredNodes,
|
||||
myNode,
|
||||
expandedCluster,
|
||||
setExpandedCluster,
|
||||
popupState,
|
||||
setPopupState,
|
||||
isVisible,
|
||||
}: NodeMarkerProps): React.ReactNode[] => {
|
||||
const { t } = useTranslation("map");
|
||||
|
||||
const { hasNodeError } = useNodeDB();
|
||||
const { focusLngLat } = useMapFitting(mapRef);
|
||||
|
||||
const selectedNode = useMemo(
|
||||
() =>
|
||||
popupState?.type !== "node"
|
||||
? undefined
|
||||
: (filteredNodes.find((node) => node.num === popupState.num) ?? undefined),
|
||||
[popupState, filteredNodes],
|
||||
);
|
||||
|
||||
const onMarkerClick = useCallback(
|
||||
(num: number, offset: PxOffset, e: { originalEvent: MouseEvent }) => {
|
||||
e.originalEvent?.stopPropagation();
|
||||
setPopupState({ type: "node", num, offset });
|
||||
const node = filteredNodes.find((node) => node.num === num) ?? undefined;
|
||||
if (node) {
|
||||
focusLngLat(toLngLat(node.position));
|
||||
}
|
||||
},
|
||||
[filteredNodes, focusLngLat, setPopupState],
|
||||
);
|
||||
|
||||
const clusters = groupNodesByIdenticalCoords(filteredNodes);
|
||||
const rendered: React.ReactNode[] = [];
|
||||
|
||||
for (const [key, nodes] of clusters) {
|
||||
if (!nodes.length || !nodes[0]?.position) {
|
||||
continue;
|
||||
}
|
||||
const [lng, lat] = toLngLat(nodes[0].position);
|
||||
const isExpanded = expandedCluster === key;
|
||||
|
||||
// Precompute pixel offsets for expanded state
|
||||
const expandedOffsets = isExpanded ? fanOutOffsetsPx(nodes.length, key) : undefined;
|
||||
|
||||
// Always render all node markers in the cluster
|
||||
for (const [i, node] of nodes.entries()) {
|
||||
const isHead = i === 0;
|
||||
|
||||
rendered.push(
|
||||
<NodeMarker
|
||||
key={`node-${key}-${node.num}`}
|
||||
id={node.num}
|
||||
lng={lng}
|
||||
lat={lat}
|
||||
offset={expandedOffsets?.[i]}
|
||||
label={node.user?.shortName ?? t("unknown.shortName")}
|
||||
tooltipLabel={node.user?.longName ?? t("unknown.longName")}
|
||||
hasError={hasNodeError(node.num)}
|
||||
isFavorite={node.isFavorite ?? false}
|
||||
isVisible={isVisible}
|
||||
onClick={(num, e) => {
|
||||
e.originalEvent?.stopPropagation();
|
||||
if (!isExpanded && !isHead) {
|
||||
// collapsed: tapping a buried marker expands the stack first
|
||||
setExpandedCluster(key);
|
||||
return;
|
||||
}
|
||||
onMarkerClick(num, expandedOffsets?.[i] ?? [0, 0], e);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (nodes.length > 1) {
|
||||
rendered.push(
|
||||
<StackBadge
|
||||
key={`stack-badge-${key}`}
|
||||
lng={lng}
|
||||
lat={lat}
|
||||
count={nodes.length - 1}
|
||||
isVisible={isVisible && !isExpanded}
|
||||
onClick={(e) => {
|
||||
e.originalEvent?.stopPropagation();
|
||||
setExpandedCluster(key);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedNode) {
|
||||
rendered.push(
|
||||
<SourcePrecisionCircles
|
||||
key={`precision-circles-selected-${selectedNode.num}`}
|
||||
data={generatePrecisionCircles([selectedNode])}
|
||||
id={`precisionCircles-selected-${selectedNode.num}`}
|
||||
isVisible={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
const [lng, lat] = toLngLat(selectedNode.position);
|
||||
|
||||
rendered.push(
|
||||
<PopupWrapper
|
||||
key={`popup-nodeinfo-${selectedNode.num}`}
|
||||
lng={lng}
|
||||
lat={lat}
|
||||
offset={popupState?.type === "node" ? popupState.offset : [0, 0]}
|
||||
onClose={() => setPopupState(undefined)}
|
||||
>
|
||||
<NodeDetail node={selectedNode} />
|
||||
</PopupWrapper>,
|
||||
);
|
||||
}
|
||||
|
||||
if (myNode && hasPos(myNode.position)) {
|
||||
const [lng, lat] = toLngLat(myNode.position);
|
||||
rendered.push(
|
||||
<NodeMarker
|
||||
key={`node-${myNode.num}`}
|
||||
id={myNode.num}
|
||||
lng={lng}
|
||||
lat={lat}
|
||||
label={myNode.user?.shortName?.toString() ?? String(myNode.num)}
|
||||
tooltipLabel={t("myNode.tooltip")}
|
||||
hasError={false}
|
||||
isFavorite={true}
|
||||
onClick={(_, e) => onMarkerClick(myNode.num, [0, 0], e)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return rendered;
|
||||
};
|
||||
Reference in New Issue
Block a user