|
|
|
@ -1,10 +1,11 @@ |
|
|
|
import { useCallback, useMemo, useState } from "react"; |
|
|
|
import type { Protobuf } from "@meshtastic/core"; |
|
|
|
import { Protobuf } from "@meshtastic/core"; |
|
|
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; |
|
|
|
|
|
|
|
interface BooleanFilter { |
|
|
|
key: string; |
|
|
|
label: string; |
|
|
|
group: string; |
|
|
|
type: "boolean"; |
|
|
|
predicate: (node: Protobuf.Mesh.NodeInfo, value: boolean) => boolean; |
|
|
|
} |
|
|
|
@ -12,6 +13,7 @@ interface BooleanFilter { |
|
|
|
interface RangeFilter { |
|
|
|
key: string; |
|
|
|
label: string; |
|
|
|
group: string; |
|
|
|
type: "range"; |
|
|
|
bounds: [number, number]; |
|
|
|
predicate: (node: Protobuf.Mesh.NodeInfo, value: [number, number]) => boolean; |
|
|
|
@ -20,16 +22,31 @@ interface RangeFilter { |
|
|
|
interface SearchFilter { |
|
|
|
key: string; |
|
|
|
label: string; |
|
|
|
group: string; |
|
|
|
type: "search"; |
|
|
|
predicate: (node: Protobuf.Mesh.NodeInfo, value: string) => boolean; |
|
|
|
} |
|
|
|
|
|
|
|
export type FilterConfig = BooleanFilter | RangeFilter | SearchFilter; |
|
|
|
interface MultiFilter { |
|
|
|
key: string; |
|
|
|
label: string; |
|
|
|
group: string; |
|
|
|
type: "multi"; |
|
|
|
options: string[]; |
|
|
|
predicate: (node: Protobuf.Mesh.NodeInfo, value: string[]) => boolean; |
|
|
|
} |
|
|
|
|
|
|
|
export type FilterConfig = |
|
|
|
| BooleanFilter |
|
|
|
| RangeFilter |
|
|
|
| SearchFilter |
|
|
|
| MultiFilter; |
|
|
|
|
|
|
|
export type FilterValueMap = { |
|
|
|
[C in FilterConfig as C["key"]]: C extends BooleanFilter ? boolean |
|
|
|
: C extends RangeFilter ? [number, number] |
|
|
|
: C extends SearchFilter ? string |
|
|
|
: C extends MultiFilter ? string[] |
|
|
|
: never; |
|
|
|
}; |
|
|
|
|
|
|
|
@ -38,6 +55,7 @@ export const filterConfigs: FilterConfig[] = [ |
|
|
|
{ |
|
|
|
key: "searchText", |
|
|
|
label: "Node name/number", |
|
|
|
group: "General", |
|
|
|
type: "search", |
|
|
|
predicate: (node, text: string) => { |
|
|
|
if (!text) return true; |
|
|
|
@ -51,25 +69,58 @@ export const filterConfigs: FilterConfig[] = [ |
|
|
|
nodeNumHex.includes(search.replace(/!/g, "")); |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "hopRange", |
|
|
|
label: "Number of hops", |
|
|
|
group: "General", |
|
|
|
type: "range", |
|
|
|
bounds: [0, 7], |
|
|
|
predicate: (node, [min, max]: [number, number]) => { |
|
|
|
const hops = node.hopsAway ?? 7; |
|
|
|
return hops >= min && hops <= max; |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "lastHeard", |
|
|
|
label: "Last heard", |
|
|
|
group: "General", |
|
|
|
type: "range", |
|
|
|
bounds: [0, 864000], // 10 days
|
|
|
|
predicate: (node, [min, max]: [number, number]) => { |
|
|
|
const secondsAgo = Date.now() / 1000 - node.lastHeard; |
|
|
|
return (secondsAgo >= min && secondsAgo <= max) || |
|
|
|
(secondsAgo >= min && max == 864000); |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "favOnly", |
|
|
|
label: "Show favourites only", |
|
|
|
group: "General", |
|
|
|
type: "boolean", |
|
|
|
predicate: (node, favOnly: boolean) => !favOnly || node.isFavorite, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "hopRange", |
|
|
|
label: "Number of hops", |
|
|
|
key: "viaMqtt", |
|
|
|
label: "Hide MQTT-connected nodes", |
|
|
|
group: "General", |
|
|
|
type: "boolean", |
|
|
|
predicate: (node, hide: boolean) => !hide || !node.viaMqtt, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "snr", |
|
|
|
label: "SNR (db)", |
|
|
|
group: "Metrics", |
|
|
|
type: "range", |
|
|
|
bounds: [0, 7], |
|
|
|
bounds: [-20, 10], |
|
|
|
predicate: (node, [min, max]: [number, number]) => { |
|
|
|
const hops = node.hopsAway ?? 7; |
|
|
|
return hops >= min && hops <= max; |
|
|
|
const snr = node.snr ?? -20; |
|
|
|
return snr >= min && snr <= max; |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "channelUtilization", |
|
|
|
label: "Channel Utilization (%)", |
|
|
|
group: "Metrics", |
|
|
|
type: "range", |
|
|
|
bounds: [0, 100], |
|
|
|
predicate: (node, [min, max]: [number, number]) => { |
|
|
|
@ -80,6 +131,7 @@ export const filterConfigs: FilterConfig[] = [ |
|
|
|
{ |
|
|
|
key: "airUtilTx", |
|
|
|
label: "Airtime Utilization (%)", |
|
|
|
group: "Metrics", |
|
|
|
type: "range", |
|
|
|
bounds: [0, 100], |
|
|
|
predicate: (node, [min, max]: [number, number]) => { |
|
|
|
@ -90,6 +142,7 @@ export const filterConfigs: FilterConfig[] = [ |
|
|
|
{ |
|
|
|
key: "battery", |
|
|
|
label: "Battery level (%)", |
|
|
|
group: "Metrics", |
|
|
|
type: "range", |
|
|
|
bounds: [0, 101], |
|
|
|
predicate: (node, [min, max]: [number, number]) => { |
|
|
|
@ -98,10 +151,52 @@ export const filterConfigs: FilterConfig[] = [ |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "viaMqtt", |
|
|
|
label: "Hide MQTT-connected nodes", |
|
|
|
type: "boolean", |
|
|
|
predicate: (node, hide: boolean) => !hide || !node.viaMqtt, |
|
|
|
key: "voltage", |
|
|
|
label: "Battery voltage (V)", |
|
|
|
group: "Metrics", |
|
|
|
type: "range", |
|
|
|
bounds: [0.1, 5.0], |
|
|
|
predicate: (node, [min, max]: [number, number]) => { |
|
|
|
const batt = node.deviceMetrics?.voltage ?? 5; |
|
|
|
return batt >= min && batt <= max; |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "role", |
|
|
|
label: "Role", |
|
|
|
group: "Role", |
|
|
|
type: "multi", |
|
|
|
options: Object.keys(Protobuf.Config.Config_DeviceConfig_Role) |
|
|
|
.filter((k) => isNaN(Number(k))) |
|
|
|
.map((k) => { |
|
|
|
const spaced = k.replace(/_/g, " "); |
|
|
|
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase(); |
|
|
|
}), |
|
|
|
predicate: (node, selected) => { |
|
|
|
return selected.map((k) => { |
|
|
|
const unSpaced = k.replace(/ /g, "_"); |
|
|
|
return unSpaced.toUpperCase(); |
|
|
|
}).includes( |
|
|
|
Protobuf.Config.Config_DeviceConfig_Role[node.user?.role ?? 0], |
|
|
|
); |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: "hwModel", |
|
|
|
label: "Hardware model", |
|
|
|
group: "Hardware", |
|
|
|
type: "multi", |
|
|
|
options: Object.keys(Protobuf.Mesh.HardwareModel) |
|
|
|
.filter((k) => isNaN(Number(k))) |
|
|
|
.map((k) => { |
|
|
|
return k.replace(/_/g, " "); |
|
|
|
}), |
|
|
|
predicate: (node, selected) => { |
|
|
|
return selected.map((k) => { |
|
|
|
const unSpaced = k.replace(/ /g, "_"); |
|
|
|
return unSpaced.toUpperCase(); |
|
|
|
}).includes(Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]); |
|
|
|
}, |
|
|
|
}, |
|
|
|
]; |
|
|
|
|
|
|
|
@ -118,6 +213,9 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) { |
|
|
|
case "search": |
|
|
|
acc[cfg.key] = ""; |
|
|
|
break; |
|
|
|
case "multi": |
|
|
|
acc[cfg.key] = cfg.options; |
|
|
|
break; |
|
|
|
} |
|
|
|
return acc; |
|
|
|
}, {} as FilterValueMap); |
|
|
|
@ -127,6 +225,15 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) { |
|
|
|
defaultState, |
|
|
|
); |
|
|
|
|
|
|
|
const groupedFilterConfigs = useMemo(() => { |
|
|
|
return filterConfigs.reduce<Record<string, FilterConfig[]>>((acc, cfg) => { |
|
|
|
const g = "group" in cfg ? cfg.group : "General"; |
|
|
|
if (!acc[g]) acc[g] = []; |
|
|
|
acc[g].push(cfg); |
|
|
|
return acc; |
|
|
|
}, {}); |
|
|
|
}, [filterConfigs]); |
|
|
|
|
|
|
|
const resetFilters = useCallback(() => { |
|
|
|
setFilters(defaultState); |
|
|
|
}, [defaultState]); |
|
|
|
@ -148,7 +255,7 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) { |
|
|
|
if (typeof val !== "boolean") return true; |
|
|
|
return cfg.predicate(node, val); |
|
|
|
|
|
|
|
case "range": |
|
|
|
case "range": { |
|
|
|
if ( |
|
|
|
!Array.isArray(val) || |
|
|
|
val.length !== 2 || |
|
|
|
@ -157,8 +264,16 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) { |
|
|
|
) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
return cfg.predicate(node, val); |
|
|
|
|
|
|
|
const tuple: [number, number] = [val[0], val[1]]; |
|
|
|
return cfg.predicate(node, tuple); |
|
|
|
} |
|
|
|
case "multi": { |
|
|
|
const safeArray = (() => { |
|
|
|
if (!Array.isArray(val)) return []; |
|
|
|
return val.filter((x): x is string => typeof x === "string"); |
|
|
|
})(); |
|
|
|
return cfg.predicate(node, safeArray); |
|
|
|
} |
|
|
|
case "search": |
|
|
|
if (typeof val !== "string") return true; |
|
|
|
return cfg.predicate(node, val); |
|
|
|
@ -174,6 +289,6 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) { |
|
|
|
onFilterChange, |
|
|
|
resetFilters, |
|
|
|
filteredNodes, |
|
|
|
filterConfigs, |
|
|
|
groupedFilterConfigs, |
|
|
|
}; |
|
|
|
} |
|
|
|
|