Browse Source

Merge remote-tracking branch 'origin/master' into traceroute

pull/211/head
Hunter Thornsberry 2 years ago
parent
commit
bcac95e7ed
  1. 94
      .github/ISSUE_TEMPLATE/bug.yml
  2. 17
      .github/ISSUE_TEMPLATE/feature.yml
  3. 2
      .github/workflows/ci.yml
  4. 2
      .github/workflows/pr.yml
  5. 2
      package.json
  6. 1864
      pnpm-lock.yaml
  7. 4
      src/components/DeviceSelector.tsx
  8. 7
      src/components/Dialog/DialogManager.tsx
  9. 21
      src/components/Dialog/QRDialog.tsx
  10. 52
      src/components/Dialog/RemoveNodeDialog.tsx
  11. 34
      src/components/PageComponents/Channel.tsx
  12. 5
      src/components/PageComponents/Config/Display.tsx
  13. 5
      src/components/PageComponents/Config/LoRa.tsx
  14. 6
      src/components/PageComponents/Config/Position.tsx
  15. 3
      src/components/PageComponents/ModuleConfig/DetectionSensor.tsx
  16. 3
      src/components/PageComponents/ModuleConfig/MQTT.tsx
  17. 5
      src/components/PageComponents/ModuleConfig/NeighborInfo.tsx
  18. 3
      src/components/PageComponents/ModuleConfig/Paxcounter.tsx
  19. 3
      src/components/PageComponents/ModuleConfig/RangeTest.tsx
  20. 4
      src/components/PageComponents/ModuleConfig/Telemetry.tsx
  21. 14
      src/components/Sidebar.tsx
  22. 4
      src/components/Toaster.tsx
  23. 2
      src/components/UI/Command.tsx
  24. 2
      src/components/UI/Input.tsx
  25. 7
      src/core/stores/appStore.ts
  26. 17
      src/core/stores/deviceStore.ts
  27. 29
      src/pages/Nodes.tsx
  28. 9
      src/validation/channel.ts
  29. 4
      src/validation/config/position.ts
  30. 4
      src/validation/config/power.ts

94
.github/ISSUE_TEMPLATE/bug.yml

@ -0,0 +1,94 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: hardware
attributes:
label: Hardware
description: What hardware are you encountering this issue on?
multiple: true
options:
- Not Applicable
- T-Beam
- T-Beam S3
- T-Beam 0.7
- T-Lora v1
- T-Lora v1.3
- T-Lora v2 1.6
- T-Deck
- T-Echo
- T-Watch
- Rak4631
- Rak11200
- Rak11310
- Heltec v1
- Heltec v2
- Heltec v2.1
- Heltec V3
- Heltec Wireless Paper
- Heltec Wireless Tracker
- Raspberry Pi Pico (W)
- Relay v1
- Relay v2
- DIY
- Other
validations:
required: true
- type: dropdown
id: category
attributes:
label: Connection Type
description: How are you connecting to your device?
multiple: true
options:
- HTTP
- Bluetooth
- Serial
validations:
required: true
- type: dropdown
id: local
attributes:
label: Local or Hosted
description: Are you using `meshtastic.local` or `client.meshtastic.org`?
multiple: true
options:
- http://meshtastic.local
- https://client.meshtastic.org
validations:
required: true
- type: input
id: version
attributes:
label: Firmware Version
description: This can be found on the device's screen or via one of the apps.
placeholder: x.x.x.yyyyyyy
validations:
required: true
- type: textarea
id: body
attributes:
label: Description
description: Please provide details on what steps you performed for this to happen.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant console output
description: If you have any log output to help in diagnosing your bug, please provide it here.
render: Shell
validations:
required: false

17
.github/ISSUE_TEMPLATE/feature.yml

@ -0,0 +1,17 @@
name: Feature Request
description: Request a new feature
title: "[Feature Request]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for your request this will not gurantee that we will implement it, but it will be reviewed.
- type: textarea
id: body
attributes:
label: Description
description: Please provide details about your enhancement.
validations:
required: true

2
.github/workflows/ci.yml

@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v4
with: with:
version: latest version: latest

2
.github/workflows/pr.yml

@ -10,7 +10,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v4
with: with:
version: latest version: latest

2
package.json

@ -22,7 +22,7 @@
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^1.8.0", "@bufbuild/protobuf": "^1.8.0",
"@emeraldpay/hashicon-react": "^0.5.2", "@emeraldpay/hashicon-react": "^0.5.2",
"@meshtastic/js": "2.3.3-0", "@meshtastic/js": "2.3.4-0",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",

1864
pnpm-lock.yaml

File diff suppressed because it is too large

4
src/components/DeviceSelector.tsx

@ -10,7 +10,7 @@ import {
MoonIcon, MoonIcon,
PlusIcon, PlusIcon,
SunIcon, SunIcon,
TerminalIcon, SearchIcon,
} from "lucide-react"; } from "lucide-react";
export const DeviceSelector = (): JSX.Element => { export const DeviceSelector = (): JSX.Element => {
@ -73,7 +73,7 @@ export const DeviceSelector = (): JSX.Element => {
className="transition-all hover:text-accent" className="transition-all hover:text-accent"
onClick={() => setCommandPaletteOpen(true)} onClick={() => setCommandPaletteOpen(true)}
> >
<TerminalIcon /> <SearchIcon />
</button> </button>
<button type="button" className="transition-all hover:text-accent"> <button type="button" className="transition-all hover:text-accent">
<LanguagesIcon /> <LanguagesIcon />

7
src/components/Dialog/DialogManager.tsx

@ -3,6 +3,7 @@ import { ImportDialog } from "@components/Dialog/ImportDialog.js";
import { QRDialog } from "@components/Dialog/QRDialog.js"; import { QRDialog } from "@components/Dialog/QRDialog.js";
import { RebootDialog } from "@components/Dialog/RebootDialog.js"; import { RebootDialog } from "@components/Dialog/RebootDialog.js";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js"; import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js"
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
export const DialogManager = (): JSX.Element => { export const DialogManager = (): JSX.Element => {
@ -42,6 +43,12 @@ export const DialogManager = (): JSX.Element => {
setDialogOpen("deviceName", open); setDialogOpen("deviceName", open);
}} }}
/> />
<RemoveNodeDialog
open={dialog.nodeRemoval}
onOpenChange={(open) => {
setDialogOpen("nodeRemoval", open);
}}
/>
</> </>
); );
}; };

21
src/components/Dialog/QRDialog.tsx

@ -30,6 +30,7 @@ export const QRDialog = ({
}: QRDialogProps): JSX.Element => { }: QRDialogProps): JSX.Element => {
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]); const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
const [qrCodeUrl, setQrCodeUrl] = useState<string>(""); const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
const allChannels = Array.from(channels.values()); const allChannels = Array.from(channels.values());
@ -49,8 +50,8 @@ export const QRDialog = ({
.replace(/\+/g, "-") .replace(/\+/g, "-")
.replace(/\//g, "_"); .replace(/\//g, "_");
setQrCodeUrl(`https://meshtastic.org/e/#${base64}`); setQrCodeUrl(`https://meshtastic.org/e/#${base64}${qrCodeAdd ? "?add=true" : ""}`);
}, [channels, selectedChannels, loraConfig]); }, [channels, selectedChannels, qrCodeAdd, loraConfig]);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@ -94,6 +95,22 @@ export const QRDialog = ({
</div> </div>
<QRCode value={qrCodeUrl} size={200} qrStyle="dots" /> <QRCode value={qrCodeUrl} size={200} qrStyle="dots" />
</div> </div>
<div className="flex justify-center">
<button
type="button"
className={ "border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") }
onClick={() => setQrCodeAdd(true)}
>
Add Channels
</button>
<button
type="button"
className={ "border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (!qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") }
onClick={() => setQrCodeAdd(false)}
>
Replace Channels
</button>
</div>
</div> </div>
<DialogFooter> <DialogFooter>
<Label>Sharable URL</Label> <Label>Sharable URL</Label>

52
src/components/Dialog/RemoveNodeDialog.tsx

@ -0,0 +1,52 @@
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore.js";
import { Button } from "@components/UI/Button.js";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
import { Label } from "@components/UI/Label.js";
export interface RemoveNodeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const RemoveNodeDialog = ({
open,
onOpenChange,
}: RemoveNodeDialogProps): JSX.Element => {
const { connection, nodes, removeNode } = useDevice();
const { nodeNumToBeRemoved } = useAppStore();
const onSubmit = () => {
connection?.removeNodeByNum(nodeNumToBeRemoved);
removeNode(nodeNumToBeRemoved);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Remove Node?</DialogTitle>
<DialogDescription>
Are you sure you want to remove this Node?
</DialogDescription>
</DialogHeader>
<div className="gap-4">
<form onSubmit={onSubmit}>
<Label>{nodes.get(nodeNumToBeRemoved)?.user?.longName}</Label>
</form>
</div>
<DialogFooter>
<Button variant="destructive" onClick={() => onSubmit()}>Remove</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

34
src/components/PageComponents/Channel.tsx

@ -1,4 +1,4 @@
import type { ChannelValidation } from "@app/validation/channel.js"; import type{ ChannelValidation } from "@app/validation/channel.js";
import { DynamicForm } from "@components/Form/DynamicForm.js"; import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useToast } from "@core/hooks/useToast.js"; import { useToast } from "@core/hooks/useToast.js";
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
@ -10,7 +10,7 @@ export interface SettingsPanelProps {
} }
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => { export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
const { connection, addChannel } = useDevice(); const { config, connection, addChannel } = useDevice();
const { toast } = useToast(); const { toast } = useToast();
const onSubmit = (data: ChannelValidation) => { const onSubmit = (data: ChannelValidation) => {
@ -19,6 +19,9 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
settings: { settings: {
...data.settings, ...data.settings,
psk: toByteArray(data.settings.psk ?? ""), psk: toByteArray(data.settings.psk ?? ""),
moduleSettings: {
positionPrecision: data.settings.positionEnabled ? data.settings.preciseLocation ? 32 : data.settings.positionPrecision : 0,
}
}, },
}); });
connection?.setChannel(channel).then(() => { connection?.setChannel(channel).then(() => {
@ -40,6 +43,9 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
settings: { settings: {
...channel?.settings, ...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)), psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
positionEnabled: channel?.settings?.moduleSettings?.positionPrecision != undefined && channel?.settings?.moduleSettings?.positionPrecision > 0,
preciseLocation: channel?.settings?.moduleSettings?.positionPrecision == 32,
positionPrecision: channel?.settings?.moduleSettings?.positionPrecision == undefined ? 10 : channel?.settings?.moduleSettings?.positionPrecision
}, },
}, },
}} }}
@ -86,6 +92,30 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
label: "Downlink Enabled", label: "Downlink Enabled",
description: "Send messages from MQTT to the local mesh", description: "Send messages from MQTT to the local mesh",
}, },
{
type: "toggle",
name: "settings.positionEnabled",
label: "Allow Position Requests",
description: "Send position to channel",
},
{
type: "toggle",
name: "settings.preciseLocation",
label: "Precise Location",
description: "Send precise location to channel",
},
{
type: "select",
name: "settings.positionPrecision",
label: "Approximate Location",
description:
"If not sharing precise location, position shared on channel will be accurate within this distance",
properties: {
enumValue: config.display?.units == 0 ?
{ "Within 23 km":10, "Within 12 km":11, "Within 5.8 km":12, "Within 2.9 km":13, "Within 1.5 km":14, "Within 700 m":15, "Within 350 m":16, "Within 200 m":17, "Within 90 m":18, "Within 50 m":19 } :
{ "Within 15 miles":10, "Within 7.3 miles":11, "Within 3.6 miles":12, "Within 1.8 miles":13, "Within 0.9 miles":14, "Within 0.5 miles":15, "Within 0.2 miles":16, "Within 600 feet":17, "Within 300 feet":18, "Within 150 feet":19 }
},
},
], ],
}, },
]} ]}

5
src/components/PageComponents/Config/Display.tsx

@ -32,7 +32,7 @@ export const Display = (): JSX.Element => {
label: "Screen Timeout", label: "Screen Timeout",
description: "Turn off the display after this long", description: "Turn off the display after this long",
properties: { properties: {
suffix: "seconds", suffix: "Seconds",
}, },
}, },
{ {
@ -50,6 +50,9 @@ export const Display = (): JSX.Element => {
name: "autoScreenCarouselSecs", name: "autoScreenCarouselSecs",
label: "Carousel Delay", label: "Carousel Delay",
description: "How fast to cycle through windows", description: "How fast to cycle through windows",
properties: {
suffix: "Seconds",
},
}, },
{ {
type: "toggle", type: "toggle",

5
src/components/PageComponents/Config/LoRa.tsx

@ -36,10 +36,13 @@ export const LoRa = (): JSX.Element => {
}, },
}, },
{ {
type: "number", type: "select",
name: "hopLimit", name: "hopLimit",
label: "Hop Limit", label: "Hop Limit",
description: "Maximum number of hops", description: "Maximum number of hops",
properties: {
enumValue: {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7}
},
}, },
{ {
type: "number", type: "number",

6
src/components/PageComponents/Config/Position.tsx

@ -94,12 +94,18 @@ export const Position = (): JSX.Element => {
name: "positionBroadcastSecs", name: "positionBroadcastSecs",
label: "Broadcast Interval", label: "Broadcast Interval",
description: "How often your position is sent out over the mesh", description: "How often your position is sent out over the mesh",
properties: {
suffix: "Seconds",
},
}, },
{ {
type: "number", type: "number",
name: "gpsUpdateInterval", name: "gpsUpdateInterval",
label: "GPS Update Interval", label: "GPS Update Interval",
description: "How often a GPS fix should be acquired", description: "How often a GPS fix should be acquired",
properties: {
suffix: "Seconds",
},
}, },
{ {
type: "number", type: "number",

3
src/components/PageComponents/ModuleConfig/DetectionSensor.tsx

@ -38,6 +38,9 @@ export const DetectionSensor = (): JSX.Element => {
label: "Minimum Broadcast Seconds", label: "Minimum Broadcast Seconds",
description: description:
"The interval in seconds of how often we can send a message to the mesh when a state change is detected", "The interval in seconds of how often we can send a message to the mesh when a state change is detected",
properties: {
suffix: "Seconds",
},
disabledBy: [ disabledBy: [
{ {
fieldName: "enabled", fieldName: "enabled",

3
src/components/PageComponents/ModuleConfig/MQTT.tsx

@ -138,6 +138,9 @@ export const MQTT = (): JSX.Element => {
name: "mapReportSettings.publishIntervalSecs", name: "mapReportSettings.publishIntervalSecs",
label: "Map Report Publish Interval (s)", label: "Map Report Publish Interval (s)",
description: "Interval in seconds to publish map reports", description: "Interval in seconds to publish map reports",
properties: {
suffix: "Seconds",
},
disabledBy: [ disabledBy: [
{ {
fieldName: "enabled", fieldName: "enabled",

5
src/components/PageComponents/ModuleConfig/NeighborInfo.tsx

@ -36,8 +36,11 @@ export const NeighborInfo = (): JSX.Element => {
type: "number", type: "number",
name: "updateInterval", name: "updateInterval",
label: "Update Interval", label: "Update Interval",
description: description:
"Interval in seconds of how often we should try to send our Neighbor Info to the mesh", "Interval in seconds of how often we should try to send our Neighbor Info to the mesh",
properties: {
suffix: "Seconds",
},
disabledBy: [ disabledBy: [
{ {
fieldName: "enabled", fieldName: "enabled",

3
src/components/PageComponents/ModuleConfig/Paxcounter.tsx

@ -37,6 +37,9 @@ export const Paxcounter = (): JSX.Element => {
name: "paxcounterUpdateInterval", name: "paxcounterUpdateInterval",
label: "Update Interval (seconds)", label: "Update Interval (seconds)",
description: "How long to wait between sending paxcounter packets", description: "How long to wait between sending paxcounter packets",
properties: {
suffix: "Seconds",
},
disabledBy: [ disabledBy: [
{ {
fieldName: "enabled", fieldName: "enabled",

3
src/components/PageComponents/ModuleConfig/RangeTest.tsx

@ -37,6 +37,9 @@ export const RangeTest = (): JSX.Element => {
name: "sender", name: "sender",
label: "Message Interval", label: "Message Interval",
description: "How long to wait between sending test packets", description: "How long to wait between sending test packets",
properties: {
suffix: "Seconds",
},
disabledBy: [ disabledBy: [
{ {
fieldName: "enabled", fieldName: "enabled",

4
src/components/PageComponents/ModuleConfig/Telemetry.tsx

@ -32,7 +32,7 @@ export const Telemetry = (): JSX.Element => {
label: "Query Interval", label: "Query Interval",
description: "Interval to get telemetry data", description: "Interval to get telemetry data",
properties: { properties: {
suffix: "seconds", suffix: "Seconds",
}, },
}, },
{ {
@ -41,7 +41,7 @@ export const Telemetry = (): JSX.Element => {
label: "Update Interval", label: "Update Interval",
description: "How often to send Metrics over the mesh", description: "How often to send Metrics over the mesh",
properties: { properties: {
suffix: "seconds", suffix: "Seconds",
}, },
}, },
{ {

14
src/components/Sidebar.tsx

@ -11,6 +11,8 @@ import {
MessageSquareIcon, MessageSquareIcon,
SettingsIcon, SettingsIcon,
UsersIcon, UsersIcon,
ZapIcon,
BatteryMediumIcon
} from "lucide-react"; } from "lucide-react";
export interface SidebarProps { export interface SidebarProps {
@ -58,7 +60,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
return ( return (
<div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] border-slate-300 bg-transparent dark:border-slate-700"> <div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] border-slate-300 bg-transparent dark:border-slate-700">
<div className="flex justify-between px-8 py-6"> <div className="flex justify-between px-8 pt-6">
<div> <div>
<span className="text-lg font-medium"> <span className="text-lg font-medium">
{myNode?.user?.shortName ?? "UNK"} {myNode?.user?.shortName ?? "UNK"}
@ -73,6 +75,16 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
<EditIcon size={16} /> <EditIcon size={16} />
</button> </button>
</div> </div>
<div className="px-8 pb-6">
<div className="flex items-center">
<BatteryMediumIcon size={24} viewBox={'0 0 28 24'}/>
<Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle>
</div>
<div className="flex items-center">
<ZapIcon size={24} viewBox={'0 0 36 24'}/>
<Subtle>{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts</Subtle>
</div>
</div>
<SidebarSection label="Navigation"> <SidebarSection label="Navigation">
{pages.map((link) => ( {pages.map((link) => (

4
src/components/Toaster.tsx

@ -17,8 +17,8 @@ export function Toaster() {
{toasts.map(({ id, title, description, action, ...props }) => ( {toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props}> <Toast key={id} {...props}>
<div className="grid gap-1"> <div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>} {title && <ToastTitle className="dark:text-white">{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>} {description && <ToastDescription className="dark:text-white-400">{description}</ToastDescription>}
</div> </div>
{action} {action}
<ToastClose /> <ToastClose />

2
src/components/UI/Command.tsx

@ -116,7 +116,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-md py-1.5 px-2 text-sm font-medium outline-none aria-selected:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:aria-selected:bg-slate-700", "relative flex cursor-default select-none items-center rounded-md py-1.5 px-2 text-sm font-medium outline-none aria-selected:bg-slate-100 data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50 dark:aria-selected:bg-slate-700",
className, className,
)} )}
{...props} {...props}

2
src/components/UI/Input.tsx

@ -32,7 +32,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
{...props} {...props}
/> />
{suffix && ( {suffix && (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 font-mono text-textSecondary"> <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-9 font-mono text-textSecondary">
<span className="text-gray-500 sm:text-sm">{suffix}</span> <span className="text-gray-500 sm:text-sm">{suffix}</span>
</div> </div>
)} )}

7
src/core/stores/appStore.ts

@ -26,6 +26,7 @@ interface AppState {
rasterSources: RasterSource[]; rasterSources: RasterSource[];
commandPaletteOpen: boolean; commandPaletteOpen: boolean;
darkMode: boolean; darkMode: boolean;
nodeNumToBeRemoved: number;
accent: AccentColor; accent: AccentColor;
connectDialogOpen: boolean; connectDialogOpen: boolean;
@ -38,6 +39,7 @@ interface AppState {
removeDevice: (deviceId: number) => void; removeDevice: (deviceId: number) => void;
setCommandPaletteOpen: (open: boolean) => void; setCommandPaletteOpen: (open: boolean) => void;
setDarkMode: (enabled: boolean) => void; setDarkMode: (enabled: boolean) => void;
setNodeNumToBeRemoved: (nodeNum: number) => void;
setAccent: (color: AccentColor) => void; setAccent: (color: AccentColor) => void;
setConnectDialogOpen: (open: boolean) => void; setConnectDialogOpen: (open: boolean) => void;
} }
@ -51,6 +53,7 @@ export const useAppStore = create<AppState>()((set) => ({
darkMode: window.matchMedia("(prefers-color-scheme: dark)").matches, darkMode: window.matchMedia("(prefers-color-scheme: dark)").matches,
accent: "orange", accent: "orange",
connectDialogOpen: false, connectDialogOpen: false,
nodeNumToBeRemoved: 0,
setRasterSources: (sources: RasterSource[]) => { setRasterSources: (sources: RasterSource[]) => {
set( set(
@ -99,6 +102,10 @@ export const useAppStore = create<AppState>()((set) => ({
}), }),
); );
}, },
setNodeNumToBeRemoved: (nodeNum) =>
set((state) => ({
nodeNumToBeRemoved: nodeNum
})),
setAccent(color) { setAccent(color) {
set( set(
produce<AppState>((draft) => { produce<AppState>((draft) => {

17
src/core/stores/deviceStore.ts

@ -24,7 +24,8 @@ export type DialogVariant =
| "QR" | "QR"
| "shutdown" | "shutdown"
| "reboot" | "reboot"
| "deviceName"; | "deviceName"
| "nodeRemoval";
export interface Device { export interface Device {
id: number; id: number;
@ -55,6 +56,7 @@ export interface Device {
shutdown: boolean; shutdown: boolean;
reboot: boolean; reboot: boolean;
deviceName: boolean; deviceName: boolean;
nodeRemoval: boolean;
}; };
setStatus: (status: Types.DeviceStatusEnum) => void; setStatus: (status: Types.DeviceStatusEnum) => void;
@ -76,6 +78,7 @@ export interface Device {
addMessage: (message: MessageWithState) => void; addMessage: (message: MessageWithState) => void;
addTraceRoute: (traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>) => void; addTraceRoute: (traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>) => void;
addMetadata: (from: number, metadata: Protobuf.Mesh.DeviceMetadata) => void; addMetadata: (from: number, metadata: Protobuf.Mesh.DeviceMetadata) => void;
removeNode: (nodeNum: number) => void;
setMessageState: ( setMessageState: (
type: "direct" | "broadcast", type: "direct" | "broadcast",
channelIndex: Types.ChannelNumber, channelIndex: Types.ChannelNumber,
@ -133,6 +136,7 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
shutdown: false, shutdown: false,
reboot: false, reboot: false,
deviceName: false, deviceName: false,
nodeRemoval: false,
}, },
pendingSettingsChanges: false, pendingSettingsChanges: false,
messageDraft: "", messageDraft: "",
@ -518,6 +522,17 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
}), }),
); );
}, },
removeNode: (nodeNum) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (!device) {
return;
}
device.nodes.delete(nodeNum);
})
)
},
setMessageState: ( setMessageState: (
type: "direct" | "broadcast", type: "direct" | "broadcast",
channelIndex: Types.ChannelNumber, channelIndex: Types.ChannelNumber,

29
src/pages/Nodes.tsx

@ -6,9 +6,19 @@ import { useDevice } from "@core/stores/deviceStore.js";
import { Hashicon } from "@emeraldpay/hashicon-react"; import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import { base16 } from "rfc4648"; import { base16 } from "rfc4648";
import { Button } from "@components/UI/Button.js";
import { TrashIcon } from "lucide-react";
import { useAppStore } from "@app/core/stores/appStore";
export interface DeleteNoteDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const NodesPage = (): JSX.Element => { export const NodesPage = (): JSX.Element => {
const { nodes, hardware } = useDevice(); const { nodes, hardware, setDialogOpen } = useDevice();
const { setNodeNumToBeRemoved } = useAppStore();
const filteredNodes = Array.from(nodes.values()).filter( const filteredNodes = Array.from(nodes.values()).filter(
(n) => n.num !== hardware.myNodeNum, (n) => n.num !== hardware.myNodeNum,
@ -27,6 +37,7 @@ export const NodesPage = (): JSX.Element => {
{ title: "Last Heard", type: "normal", sortable: true }, { title: "Last Heard", type: "normal", sortable: true },
{ title: "SNR", type: "normal", sortable: true }, { title: "SNR", type: "normal", sortable: true },
{ title: "Connection", type: "normal", sortable: true }, { title: "Connection", type: "normal", sortable: true },
{ title: "Remove", type: "normal", sortable: false },
]} ]}
rows={filteredNodes.map((node) => [ rows={filteredNodes.map((node) => [
<Hashicon size={24} value={node.num.toString()} />, <Hashicon size={24} value={node.num.toString()} />,
@ -57,12 +68,16 @@ export const NodesPage = (): JSX.Element => {
{(node.snr + 10) * 5}raw {(node.snr + 10) * 5}raw
</Mono>, </Mono>,
<Mono> <Mono>
{node.lastHeard != 0 ? {node.lastHeard != 0 ?
(node.viaMqtt === false && node.hopsAway === 0 (node.viaMqtt === false && node.hopsAway === 0
? "Direct": node.hopsAway.toString() + " hops away") ? "Direct": node.hopsAway.toString() + " hops away")
: "-"} : "-"}
{node.viaMqtt === true? ", via MQTT": ""} {node.viaMqtt === true? ", via MQTT": ""}
</Mono> </Mono>,
<Button variant="destructive" onClick={() => {
setNodeNumToBeRemoved(node.num);
setDialogOpen("nodeRemoval", true)
}}><TrashIcon />Remove</Button>
])} ])}
/> />
</div> </div>

9
src/validation/channel.ts

@ -41,4 +41,13 @@ export class Channel_SettingsValidation
@IsBoolean() @IsBoolean()
downlinkEnabled: boolean; downlinkEnabled: boolean;
@IsBoolean()
positionEnabled: boolean;
@IsBoolean()
preciseLocation: boolean;
@IsInt()
positionPrecision: number;
} }

4
src/validation/config/position.ts

@ -2,8 +2,10 @@ import type { Message } from "@bufbuild/protobuf";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import { IsArray, IsBoolean, IsEnum, IsInt } from "class-validator"; import { IsArray, IsBoolean, IsEnum, IsInt } from "class-validator";
const DeprecatedPositionValidationFields = ['gpsEnabled', 'gpsAttemptTime'];
export class PositionValidation export class PositionValidation
implements Omit<Protobuf.Config.Config_PositionConfig, keyof Message> implements Omit<Protobuf.Config.Config_PositionConfig, keyof Message | typeof DeprecatedPositionValidationFields[number]>
{ {
@IsInt() @IsInt()
positionBroadcastSecs: number; positionBroadcastSecs: number;

4
src/validation/config/power.ts

@ -1,6 +1,6 @@
import type { Message } from "@bufbuild/protobuf"; import type { Message } from "@bufbuild/protobuf";
import type { Protobuf } from "@meshtastic/js"; import type { Protobuf } from "@meshtastic/js";
import { IsBoolean, IsInt, Max, Min } from "class-validator"; import { IsBoolean, IsInt, IsNumber, Max, Min } from "class-validator";
export class PowerValidation export class PowerValidation
implements Omit<Protobuf.Config.Config_PowerConfig, keyof Message> implements Omit<Protobuf.Config.Config_PowerConfig, keyof Message>
@ -11,7 +11,7 @@ export class PowerValidation
@IsInt() @IsInt()
onBatteryShutdownAfterSecs: number; onBatteryShutdownAfterSecs: number;
@IsInt() @IsNumber()
@Min(2) @Min(2)
@Max(4) @Max(4)
adcMultiplierOverride: number; adcMultiplierOverride: number;

Loading…
Cancel
Save