WIP updates
This commit is contained in:
@@ -19,13 +19,12 @@ jobs:
|
|||||||
# Checks-out repository
|
# Checks-out repository
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- uses: pnpm/action-setup@v2.0.1
|
- uses: pnpm/action-setup@v2.2.4
|
||||||
with:
|
with:
|
||||||
version: 6.14.3
|
version: latest
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: latest
|
||||||
cache: 'pnpm'
|
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- run: pnpm format
|
- run: pnpm format
|
||||||
- run: pnpm build
|
- run: pnpm build
|
||||||
@@ -33,10 +32,10 @@ jobs:
|
|||||||
|
|
||||||
# Upload Artifact
|
# Upload Artifact
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: 'marvinpinto/action-automatic-releases@latest'
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
with:
|
with:
|
||||||
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
automatic_release_tag: 'latest'
|
automatic_release_tag: "latest"
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: |
|
files: |
|
||||||
./dist/build.tar
|
./dist/build.tar
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Ignore artifacts:
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
stats.html
|
||||||
+38
-29
@@ -9,41 +9,42 @@
|
|||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)",
|
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)",
|
||||||
"format": "prettier --write 'src/**/*.{ts,tsx}' && eslint src/*.{ts,tsx}",
|
"format:check": "prettier --check . && eslint src/**/*.{ts,tsx}",
|
||||||
|
"format:fix": "prettier --write . && eslint --fix src/**/*.{ts,tsx}",
|
||||||
"check:unimported": "unimported"
|
"check:unimported": "unimported"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/meshtastic/meshtastic-web.git"
|
"url": "git+https://github.com/meshtastic/web.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/meshtastic/meshtastic-web/issues"
|
"url": "https://github.com/meshtastic/web/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://meshtastic.org",
|
"homepage": "https://meshtastic.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||||
"@headlessui/react": "^1.7.3",
|
"@headlessui/react": "^1.7.4",
|
||||||
"@heroicons/react": "^2.0.12",
|
"@heroicons/react": "^2.0.13",
|
||||||
|
"@hookform/error-message": "^2.0.1",
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@meshtastic/eslint-config": "^1.0.8",
|
"@meshtastic/meshtasticjs": "^0.7.0",
|
||||||
"@meshtastic/meshtasticjs": "^0.6.112",
|
|
||||||
"@tailwindcss/line-clamp": "^0.4.2",
|
"@tailwindcss/line-clamp": "^0.4.2",
|
||||||
"@tailwindcss/typography": "^0.5.7",
|
"@tailwindcss/typography": "^0.5.8",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"chart.js": "^3.9.1",
|
"chart.js": "^4.0.1",
|
||||||
"chartjs-adapter-date-fns": "^2.0.0",
|
"chartjs-adapter-date-fns": "^2.0.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
"class-validator": "^0.13.2",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"geodesy": "^2.4.0",
|
"geodesy": "^2.4.0",
|
||||||
"immer": "^9.0.16",
|
"immer": "^9.0.16",
|
||||||
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
||||||
"maplibre-gl": "^2.4.0",
|
"maplibre-gl": "2.4.0",
|
||||||
"pretty-ms": "^8.0.0",
|
"pretty-ms": "^8.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-chartjs-2": "^4.3.1",
|
"react-chartjs-2": "^5.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.38.0",
|
"react-hook-form": "^7.39.6",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-icons": "^4.6.0",
|
"react-icons": "^4.6.0",
|
||||||
"react-json-pretty": "^2.2.0",
|
"react-json-pretty": "^2.2.0",
|
||||||
@@ -51,30 +52,38 @@
|
|||||||
"react-map-gl": "^7.0.19",
|
"react-map-gl": "^7.0.19",
|
||||||
"react-qrcode-logo": "^2.8.0",
|
"react-qrcode-logo": "^2.8.0",
|
||||||
"rfc4648": "^1.5.2",
|
"rfc4648": "^1.5.2",
|
||||||
"zustand": "4.1.3"
|
"zustand": "4.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@types/chrome": "^0.0.200",
|
"@types/chrome": "^0.0.203",
|
||||||
"@types/geodesy": "^2.2.3",
|
"@types/geodesy": "^2.2.3",
|
||||||
"@types/node": "^18.11.7",
|
"@types/node": "^18.11.9",
|
||||||
"@types/react": "^18.0.23",
|
"@types/react": "^18.0.25",
|
||||||
"@types/react-dom": "^18.0.7",
|
"@types/react-dom": "^18.0.9",
|
||||||
"@types/w3c-web-serial": "^1.0.3",
|
"@types/w3c-web-serial": "^1.0.3",
|
||||||
"@types/web-bluetooth": "^0.0.16",
|
"@types/web-bluetooth": "^0.0.16",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||||
|
"@typescript-eslint/parser": "^5.44.0",
|
||||||
"@vitejs/plugin-react": "^2.2.0",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.13",
|
||||||
"gzipper": "^7.1.0",
|
"eslint": "^8.28.0",
|
||||||
"postcss": "^8.4.18",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"prettier": "^2.7.1",
|
"eslint-import-resolver-typescript": "^3.5.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-react": "^7.31.11",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"gzipper": "^7.2.0",
|
||||||
|
"postcss": "^8.4.19",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||||
"rollup-plugin-visualizer": "^5.8.3",
|
"rollup-plugin-visualizer": "^5.8.3",
|
||||||
"tailwindcss": "^3.2.1",
|
"tailwindcss": "^3.2.4",
|
||||||
"tar": "^6.1.11",
|
"tar": "^6.1.12",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.9.3",
|
||||||
"unimported": "^1.22.0",
|
"unimported": "^1.23.0",
|
||||||
"vite": "^3.2.0",
|
"vite": "^3.2.4",
|
||||||
"vite-plugin-environment": "^1.1.3"
|
"vite-plugin-environment": "^1.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+818
-697
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|||||||
+3
-2
@@ -1,3 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [require('prettier-plugin-tailwindcss')],
|
trailingComma: "none",
|
||||||
}
|
plugins: [require("prettier-plugin-tailwindcss")]
|
||||||
|
};
|
||||||
|
|||||||
+5
-1
@@ -10,6 +10,7 @@ import { useDeviceStore } from "@core/stores/deviceStore.js";
|
|||||||
import { CommandPalette } from "./components/CommandPalette/Index.js";
|
import { CommandPalette } from "./components/CommandPalette/Index.js";
|
||||||
import { DeviceSelector } from "./components/DeviceSelector.js";
|
import { DeviceSelector } from "./components/DeviceSelector.js";
|
||||||
import { DialogManager } from "./components/Dialog/DialogManager.js";
|
import { DialogManager } from "./components/Dialog/DialogManager.js";
|
||||||
|
import { Drawer } from "./components/Drawer.js";
|
||||||
import { NewDevice } from "./components/NewDevice.js";
|
import { NewDevice } from "./components/NewDevice.js";
|
||||||
import { PageNav } from "./components/PageNav.js";
|
import { PageNav } from "./components/PageNav.js";
|
||||||
import { Sidebar } from "./components/Sidebar.js";
|
import { Sidebar } from "./components/Sidebar.js";
|
||||||
@@ -30,14 +31,17 @@ export const App = (): JSX.Element => {
|
|||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<Toaster
|
<Toaster
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
duration: 2000,
|
duration: 2000
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DialogManager />
|
<DialogManager />
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<PageNav />
|
<PageNav />
|
||||||
<MapProvider>
|
<MapProvider>
|
||||||
|
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||||
<PageRouter />
|
<PageRouter />
|
||||||
|
<Drawer />
|
||||||
|
</div>
|
||||||
</MapProvider>
|
</MapProvider>
|
||||||
</DeviceWrapper>
|
</DeviceWrapper>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type React from "react";
|
|||||||
import { DeviceContext } from "@core/providers/useDevice.js";
|
import { DeviceContext } from "@core/providers/useDevice.js";
|
||||||
import type { Device } from "@core/stores/deviceStore.js";
|
import type { Device } from "@core/stores/deviceStore.js";
|
||||||
|
|
||||||
export interface DeviceProps {
|
export interface DeviceWrapperProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
device: Device;
|
device: Device;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeviceWrapper = ({
|
export const DeviceWrapper = ({
|
||||||
children,
|
children,
|
||||||
device,
|
device
|
||||||
}: DeviceProps): JSX.Element => {
|
}: DeviceWrapperProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<DeviceContext.Provider value={device}>{children}</DeviceContext.Provider>
|
<DeviceContext.Provider value={device}>{children}</DeviceContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ import { PeersPage } from "./pages/Peers.js";
|
|||||||
export const PageRouter = (): JSX.Element => {
|
export const PageRouter = (): JSX.Element => {
|
||||||
const { activePage } = useDevice();
|
const { activePage } = useDevice();
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex-grow border-b">
|
||||||
{activePage === "messages" && <MessagesPage />}
|
{activePage === "messages" && <MessagesPage />}
|
||||||
{activePage === "map" && <MapPage />}
|
{activePage === "map" && <MapPage />}
|
||||||
{activePage === "extensions" && <ExtensionsPage />}
|
{activePage === "extensions" && <ExtensionsPage />}
|
||||||
@@ -23,6 +23,6 @@ export const PageRouter = (): JSX.Element => {
|
|||||||
{activePage === "peers" && <PeersPage />}
|
{activePage === "peers" && <PeersPage />}
|
||||||
{activePage === "info" && <InfoPage />}
|
{activePage === "info" && <InfoPage />}
|
||||||
{activePage === "logs" && <LogsPage />}
|
{activePage === "logs" && <LogsPage />}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,14 +22,18 @@ export const Button = ({
|
|||||||
className={`flex w-full rounded-md border border-transparent px-3 focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
className={`flex w-full rounded-md border border-transparent px-3 focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||||
variant === "primary"
|
variant === "primary"
|
||||||
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
|
? "bg-orange-600 text-white shadow-sm hover:bg-orange-700"
|
||||||
: "bg-orange-100 text-orange-700 hover:bg-orange-200"
|
: "bg-orange-200 text-orange-700 hover:bg-orange-200"
|
||||||
} ${
|
} ${
|
||||||
size === "sm"
|
size === "sm"
|
||||||
? "h-8 text-sm"
|
? "h-8 text-sm"
|
||||||
: size === "md"
|
: size === "md"
|
||||||
? "h-10 text-sm"
|
? "h-10 text-sm"
|
||||||
: "h-10 text-base"
|
: "h-10 text-base"
|
||||||
} ${disabled ? "cursor-not-allowed bg-red-400 focus:ring-red-500" : ""}`}
|
} ${
|
||||||
|
disabled
|
||||||
|
? "cursor-not-allowed bg-gray-400 hover:bg-gray-400 focus:ring-gray-500"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
WindowIcon,
|
WindowIcon,
|
||||||
XCircleIcon,
|
XCircleIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { GroupView } from "./GroupView.js";
|
import { GroupView } from "./GroupView.js";
|
||||||
@@ -77,58 +77,58 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
icon: InboxIcon,
|
icon: InboxIcon,
|
||||||
action() {
|
action() {
|
||||||
setActivePage("messages");
|
setActivePage("messages");
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Map",
|
name: "Map",
|
||||||
icon: MapIcon,
|
icon: MapIcon,
|
||||||
action() {
|
action() {
|
||||||
setActivePage("map");
|
setActivePage("map");
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Extensions",
|
name: "Extensions",
|
||||||
icon: BeakerIcon,
|
icon: BeakerIcon,
|
||||||
action() {
|
action() {
|
||||||
setActivePage("extensions");
|
setActivePage("extensions");
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Config",
|
name: "Config",
|
||||||
icon: Cog8ToothIcon,
|
icon: Cog8ToothIcon,
|
||||||
action() {
|
action() {
|
||||||
setActivePage("config");
|
setActivePage("config");
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Channels",
|
name: "Channels",
|
||||||
icon: Square3Stack3DIcon,
|
icon: Square3Stack3DIcon,
|
||||||
action() {
|
action() {
|
||||||
setActivePage("channels");
|
setActivePage("channels");
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Peers",
|
name: "Peers",
|
||||||
icon: UsersIcon,
|
icon: UsersIcon,
|
||||||
action() {
|
action() {
|
||||||
setActivePage("peers");
|
setActivePage("peers");
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Info",
|
name: "Info",
|
||||||
icon: IdentificationIcon,
|
icon: IdentificationIcon,
|
||||||
action() {
|
action() {
|
||||||
setActivePage("info");
|
setActivePage("info");
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Logs",
|
name: "Logs",
|
||||||
icon: DocumentTextIcon,
|
icon: DocumentTextIcon,
|
||||||
action() {
|
action() {
|
||||||
setActivePage("logs");
|
setActivePage("logs");
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Manage",
|
name: "Manage",
|
||||||
@@ -138,17 +138,17 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
name: "[WIP] Switch Node",
|
name: "[WIP] Switch Node",
|
||||||
icon: ArrowsRightLeftIcon,
|
icon: ArrowsRightLeftIcon,
|
||||||
action() {
|
action() {
|
||||||
alert('This feature is not implemented');
|
alert("This feature is not implemented");
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Connect New Node",
|
name: "Connect New Node",
|
||||||
icon: PlusIcon,
|
icon: PlusIcon,
|
||||||
action() {
|
action() {
|
||||||
setSelectedDevice(0);
|
setSelectedDevice(0);
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Contextual",
|
name: "Contextual",
|
||||||
@@ -159,20 +159,20 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
icon: QrCodeIcon,
|
icon: QrCodeIcon,
|
||||||
action() {
|
action() {
|
||||||
setQRDialogOpen(true);
|
setQRDialogOpen(true);
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Reset Peers",
|
name: "Reset Peers",
|
||||||
icon: TrashIcon,
|
icon: TrashIcon,
|
||||||
action() {
|
action() {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(connection.resetPeers(), {
|
void toast.promise(connection.resetPeers({}), {
|
||||||
loading: "Resetting...",
|
loading: "Resetting...",
|
||||||
success: "Succesfully reset peers",
|
success: "Succesfully reset peers",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Disconnect",
|
name: "Disconnect",
|
||||||
@@ -181,9 +181,9 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
void connection?.disconnect();
|
void connection?.disconnect();
|
||||||
setSelectedDevice(0);
|
setSelectedDevice(0);
|
||||||
removeDevice(selectedDevice ?? 0);
|
removeDevice(selectedDevice ?? 0);
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Debug",
|
name: "Debug",
|
||||||
@@ -194,16 +194,16 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
icon: ArrowPathIcon,
|
icon: ArrowPathIcon,
|
||||||
action() {
|
action() {
|
||||||
void connection?.configure();
|
void connection?.configure();
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "[WIP] Clear Messages",
|
name: "[WIP] Clear Messages",
|
||||||
icon: ArchiveBoxXMarkIcon,
|
icon: ArchiveBoxXMarkIcon,
|
||||||
action() {
|
action() {
|
||||||
alert('This feature is not implemented');
|
alert("This feature is not implemented");
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Application",
|
name: "Application",
|
||||||
@@ -213,11 +213,11 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
name: "[WIP] Toggle Dark Mode",
|
name: "[WIP] Toggle Dark Mode",
|
||||||
icon: MoonIcon,
|
icon: MoonIcon,
|
||||||
action() {
|
action() {
|
||||||
alert('This feature is not implemented');
|
alert("This feature is not implemented");
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleKeydown = (e: KeyboardEvent) => {
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
@@ -243,7 +243,7 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
return `${group.name} ${command.name}`
|
return `${group.name} ${command.name}`
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(query.toLowerCase());
|
.includes(query.toLowerCase());
|
||||||
}),
|
})
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((group) => group.commands.length);
|
.filter((group) => group.commands.length);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface PaletteTransitionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PaletteTransition = ({
|
export const PaletteTransition = ({
|
||||||
children,
|
children
|
||||||
}: PaletteTransitionProps): JSX.Element => {
|
}: PaletteTransitionProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { fromByteArray } from "base64-js";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import { QRCode } from "react-qrcode-logo";
|
||||||
|
|
||||||
|
import { Dialog } from "@headlessui/react";
|
||||||
|
import { ClipboardIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
|
import { Checkbox } from "../form/Checkbox.js";
|
||||||
|
import { Input } from "../form/Input.js";
|
||||||
|
import { IconButton } from "../IconButton.js";
|
||||||
|
|
||||||
|
export interface ImportDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
close: () => void;
|
||||||
|
loraConfig?: Protobuf.Config_LoRaConfig;
|
||||||
|
channels: Protobuf.Channel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportDialog = ({
|
||||||
|
isOpen,
|
||||||
|
close,
|
||||||
|
loraConfig,
|
||||||
|
channels
|
||||||
|
}: ImportDialogProps): JSX.Element => {
|
||||||
|
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
|
||||||
|
const [QRCodeURL, setQRCodeURL] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const channelsToEncode = channels
|
||||||
|
.filter((channel) => selectedChannels.includes(channel.index))
|
||||||
|
.map((channel) => channel.settings)
|
||||||
|
.filter((ch): ch is Protobuf.ChannelSettings => !!ch);
|
||||||
|
const encoded = Protobuf.ChannelSet.toBinary({
|
||||||
|
loraConfig,
|
||||||
|
settings: channelsToEncode
|
||||||
|
});
|
||||||
|
const base64 = fromByteArray(encoded)
|
||||||
|
.replace(/=/g, "")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_");
|
||||||
|
|
||||||
|
setQRCodeURL(`https://meshtastic.org/e/#${base64}`);
|
||||||
|
}, [channels, selectedChannels, loraConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onClose={close}>
|
||||||
|
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<Dialog.Panel>
|
||||||
|
<div className="divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow">
|
||||||
|
<div className="flex px-4 py-5 sm:px-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-bold">Generate QR Code</h1>
|
||||||
|
<h5 className="text-sm text-slate-600">
|
||||||
|
The current LoRa configuration will also be shared.
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
onClick={close}
|
||||||
|
className="my-auto ml-auto"
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
icon={<XMarkIcon className="h-4" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 px-4 py-5 sm:p-6">
|
||||||
|
<div className="flex w-40 flex-col gap-1">
|
||||||
|
{channels.map((channel) => (
|
||||||
|
<Checkbox
|
||||||
|
key={channel.index}
|
||||||
|
disabled={
|
||||||
|
channel.index === 0 ||
|
||||||
|
channel.role === Protobuf.Channel_Role.DISABLED
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
channel.settings?.name.length
|
||||||
|
? channel.settings.name
|
||||||
|
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||||
|
? "Primary"
|
||||||
|
: `Channel: ${channel.index}`
|
||||||
|
}
|
||||||
|
checked={selectedChannels.includes(channel.index)}
|
||||||
|
onChange={() => {
|
||||||
|
if (selectedChannels.includes(channel.index)) {
|
||||||
|
setSelectedChannels(
|
||||||
|
selectedChannels.filter((c) => c !== channel.index)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedChannels([
|
||||||
|
...selectedChannels,
|
||||||
|
channel.index
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<QRCode value={QRCodeURL} size={200} qrStyle="dots" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 py-4 sm:px-6">
|
||||||
|
<Input
|
||||||
|
label="Sharable URL"
|
||||||
|
value={QRCodeURL}
|
||||||
|
disabled
|
||||||
|
action={{
|
||||||
|
icon: <ClipboardIcon className="h-4" />,
|
||||||
|
action() {
|
||||||
|
void navigator.clipboard.writeText(QRCodeURL);
|
||||||
|
toast.success("Copied URL to Clipboard");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* </Card> */}
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -24,7 +24,7 @@ export const QRDialog = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
close,
|
close,
|
||||||
loraConfig,
|
loraConfig,
|
||||||
channels,
|
channels
|
||||||
}: 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>("");
|
||||||
@@ -37,7 +37,7 @@ export const QRDialog = ({
|
|||||||
const encoded = Protobuf.ChannelSet.toBinary(
|
const encoded = Protobuf.ChannelSet.toBinary(
|
||||||
Protobuf.ChannelSet.create({
|
Protobuf.ChannelSet.create({
|
||||||
loraConfig,
|
loraConfig,
|
||||||
settings: channelsToEncode,
|
settings: channelsToEncode
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const base64 = fromByteArray(encoded)
|
const base64 = fromByteArray(encoded)
|
||||||
@@ -94,7 +94,7 @@ export const QRDialog = ({
|
|||||||
} else {
|
} else {
|
||||||
setSelectedChannels([
|
setSelectedChannels([
|
||||||
...selectedChannels,
|
...selectedChannels,
|
||||||
channel.index,
|
channel.index
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -114,7 +114,7 @@ export const QRDialog = ({
|
|||||||
action() {
|
action() {
|
||||||
void navigator.clipboard.writeText(QRCodeURL);
|
void navigator.clipboard.writeText(QRCodeURL);
|
||||||
toast.success("Copied URL to Clipboard");
|
toast.success("Copied URL to Clipboard");
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export const Drawer = (): JSX.Element => {
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const tabs = [{ title: "Notifications" }, { title: "Debug" }];
|
||||||
|
|
||||||
|
const { config, moduleConfig, hardware, nodes, waypoints, connection } =
|
||||||
|
useDevice();
|
||||||
|
|
||||||
|
const [serialLogs, setSerialLogs] = useState<string>("");
|
||||||
|
|
||||||
|
connection?.onDeviceDebugLog.subscribe((packet) => {
|
||||||
|
setSerialLogs(serialLogs + new TextDecoder().decode(packet));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`shadow-md ${drawerOpen ? "h-40" : "h-8"}`}>
|
||||||
|
<div className="flex h-8 bg-slate-50">
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setDrawerOpen(!drawerOpen);
|
||||||
|
}}
|
||||||
|
className="ml-auto flex px-2 hover:cursor-pointer hover:bg-slate-100"
|
||||||
|
>
|
||||||
|
<div className="m-auto">
|
||||||
|
{drawerOpen ? (
|
||||||
|
<ChevronDownIcon className="h-4 text-gray-700" />
|
||||||
|
) : (
|
||||||
|
<ChevronUpIcon className="h-4 text-gray-700" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${drawerOpen ? "flex" : "hidden"}`}>
|
||||||
|
<div>
|
||||||
|
{serialLogs.split("\n").map((line, index) => (
|
||||||
|
<div key={index} className="text-sm">
|
||||||
|
{line}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,7 +18,7 @@ export const Dropdown = ({
|
|||||||
stat,
|
stat,
|
||||||
icon,
|
icon,
|
||||||
defaultOpen,
|
defaultOpen,
|
||||||
children,
|
children
|
||||||
}: DropdownProps): JSX.Element => {
|
}: DropdownProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Disclosure defaultOpen={defaultOpen}>
|
<Disclosure defaultOpen={defaultOpen}>
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ export const NewDevice = () => {
|
|||||||
element: BLE,
|
element: BLE,
|
||||||
disabled: !navigator.bluetooth,
|
disabled: !navigator.bluetooth,
|
||||||
disabledMessage:
|
disabledMessage:
|
||||||
"WebBluetooth is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
|
"WebBluetooth is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "HTTP",
|
name: "HTTP",
|
||||||
icon: <FiWifi className="h-4" />,
|
icon: <FiWifi className="h-4" />,
|
||||||
element: HTTP,
|
element: HTTP,
|
||||||
disabled: false,
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Serial",
|
name: "Serial",
|
||||||
@@ -29,8 +29,8 @@ export const NewDevice = () => {
|
|||||||
element: Serial,
|
element: Serial,
|
||||||
disabled: !navigator.serial,
|
disabled: !navigator.serial,
|
||||||
disabledMessage:
|
disabledMessage:
|
||||||
"WebSerial is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
|
"WebSerial is currently only supported by Chromium based browsers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility"
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Button } from "@app/components/Button.js";
|
||||||
|
import { InfoWrapper } from "@app/components/form/InfoWrapper.js";
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
|
import { IconButton } from "@app/components/IconButton.js";
|
||||||
|
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||||
|
import { MapValidation } from "@app/validation/appConfig/map.js";
|
||||||
|
import { Form } from "@components/form/Form";
|
||||||
|
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
||||||
|
|
||||||
|
export const Map = (): JSX.Element => {
|
||||||
|
const { rasterSources, setRasterSources } = useAppStore();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isDirty },
|
||||||
|
control,
|
||||||
|
reset
|
||||||
|
} = useForm<MapValidation>({
|
||||||
|
defaultValues: {
|
||||||
|
// wmsSources: wmsSources ?? [
|
||||||
|
// {
|
||||||
|
// url: "",
|
||||||
|
// tileSize: 512,
|
||||||
|
// type: "raster"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
},
|
||||||
|
resolver: classValidatorResolver(MapValidation)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove, insert } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "rasterSources"
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
setRasterSources(data.rasterSources);
|
||||||
|
});
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// reset(rasterSources);
|
||||||
|
// }, [reset, rasterSources]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
title="Map Config"
|
||||||
|
breadcrumbs={["App Config", "Map"]}
|
||||||
|
reset={() =>
|
||||||
|
reset({
|
||||||
|
rasterSources
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dirty={isDirty}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<InfoWrapper label="WMS Sources">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex w-full gap-2">
|
||||||
|
<Controller
|
||||||
|
name={`rasterSources.${index}.enabled`}
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, ...rest } }) => (
|
||||||
|
<Toggle checked={value} {...rest} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Name"
|
||||||
|
error={
|
||||||
|
errors.rasterSources
|
||||||
|
? errors.rasterSources[index]?.title?.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
{...register(`rasterSources.${index}.title`)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Tile Size"
|
||||||
|
type="number"
|
||||||
|
error={
|
||||||
|
errors.rasterSources
|
||||||
|
? errors.rasterSources[index]?.tileSize?.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
{...register(`rasterSources.${index}.tileSize`, {
|
||||||
|
valueAsNumber: true
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="URL"
|
||||||
|
error={
|
||||||
|
errors.rasterSources
|
||||||
|
? errors.rasterSources[index]?.tiles?.message
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
{...register(`rasterSources.${index}.tiles`)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
className="shrink-0"
|
||||||
|
icon={<TrashIcon className="w-4" />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
append({
|
||||||
|
enabled: true,
|
||||||
|
title: "",
|
||||||
|
tiles: [
|
||||||
|
"https://img.nj.gov/imagerywms/Natural2015?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:3857&transparent=true&width=256&height=256&layers=Natural2015"
|
||||||
|
],
|
||||||
|
tileSize: 512
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New Source
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</InfoWrapper>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@ import { useDevice } from "@core/providers/useDevice.js";
|
|||||||
import {
|
import {
|
||||||
ArrowPathIcon,
|
ArrowPathIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
EyeSlashIcon,
|
EyeSlashIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
@@ -34,39 +34,39 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control,
|
||||||
setValue,
|
setValue
|
||||||
} = useForm<ChannelSettingsValidation>({
|
} = useForm<ChannelSettingsValidation>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
enabled: [
|
enabled: [
|
||||||
Protobuf.Channel_Role.SECONDARY,
|
Protobuf.Channel_Role.SECONDARY,
|
||||||
Protobuf.Channel_Role.PRIMARY,
|
Protobuf.Channel_Role.PRIMARY
|
||||||
].find((role) => role === channel?.role)
|
].find((role) => role === channel?.role)
|
||||||
? true
|
? true
|
||||||
: false,
|
: false,
|
||||||
...channel?.settings,
|
...channel?.settings,
|
||||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0))
|
||||||
},
|
},
|
||||||
resolver: classValidatorResolver(ChannelSettingsValidation),
|
resolver: classValidatorResolver(ChannelSettingsValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset({
|
reset({
|
||||||
enabled: [
|
enabled: [
|
||||||
Protobuf.Channel_Role.SECONDARY,
|
Protobuf.Channel_Role.SECONDARY,
|
||||||
Protobuf.Channel_Role.PRIMARY,
|
Protobuf.Channel_Role.PRIMARY
|
||||||
].find((role) => role === channel?.role)
|
].find((role) => role === channel?.role)
|
||||||
? true
|
? true
|
||||||
: false,
|
: false,
|
||||||
...channel?.settings,
|
...channel?.settings,
|
||||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0))
|
||||||
});
|
});
|
||||||
}, [channel, reset]);
|
}, [channel, reset]);
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setChannel(
|
connection.setChannel({
|
||||||
{
|
channel: {
|
||||||
role:
|
role:
|
||||||
channel?.role === Protobuf.Channel_Role.PRIMARY
|
channel?.role === Protobuf.Channel_Role.PRIMARY
|
||||||
? Protobuf.Channel_Role.PRIMARY
|
? Protobuf.Channel_Role.PRIMARY
|
||||||
@@ -76,18 +76,18 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
index: channel?.index,
|
index: channel?.index,
|
||||||
settings: {
|
settings: {
|
||||||
...data,
|
...data,
|
||||||
psk: toByteArray(data.psk ?? ""),
|
psk: toByteArray(data.psk ?? "")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: (): Promise<void> => {
|
||||||
(): Promise<void> => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Channel",
|
success: "Saved Channel",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,18 +102,18 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
? channel.settings.name
|
? channel.settings.name
|
||||||
: channel.role === Protobuf.Channel_Role.PRIMARY
|
: channel.role === Protobuf.Channel_Role.PRIMARY
|
||||||
? "Primary"
|
? "Primary"
|
||||||
: `Channel: ${channel.index}`,
|
: `Channel: ${channel.index}`
|
||||||
]}
|
]}
|
||||||
reset={() =>
|
reset={() =>
|
||||||
reset({
|
reset({
|
||||||
enabled: [
|
enabled: [
|
||||||
Protobuf.Channel_Role.SECONDARY,
|
Protobuf.Channel_Role.SECONDARY,
|
||||||
Protobuf.Channel_Role.PRIMARY,
|
Protobuf.Channel_Role.PRIMARY
|
||||||
].find((role) => role === channel?.role)
|
].find((role) => role === channel?.role)
|
||||||
? true
|
? true
|
||||||
: false,
|
: false,
|
||||||
...channel?.settings,
|
...channel?.settings,
|
||||||
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
|
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
dirty={isDirty}
|
dirty={isDirty}
|
||||||
@@ -153,8 +153,10 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
action: () => {
|
action: () => {
|
||||||
const key = new Uint8Array(keySize / 8);
|
const key = new Uint8Array(keySize / 8);
|
||||||
crypto.getRandomValues(key);
|
crypto.getRandomValues(key);
|
||||||
setValue("psk", fromByteArray(key));
|
setValue("psk", fromByteArray(key), {
|
||||||
},
|
shouldDirty: true
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value={128}>128 Bit</option>
|
<option value={128}>128 Bit</option>
|
||||||
@@ -173,7 +175,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
|||||||
),
|
),
|
||||||
action: () => {
|
action: () => {
|
||||||
setPskHidden(!pskHidden);
|
setPskHidden(!pskHidden);
|
||||||
},
|
}
|
||||||
}}
|
}}
|
||||||
error={errors.psk?.message}
|
error={errors.psk?.message}
|
||||||
{...register("psk")}
|
{...register("psk")}
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ export const Bluetooth = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
control,
|
control,
|
||||||
reset,
|
reset
|
||||||
} = useForm<BluetoothValidation>({
|
} = useForm<BluetoothValidation>({
|
||||||
defaultValues: config.bluetooth,
|
defaultValues: config.bluetooth,
|
||||||
resolver: classValidatorResolver(BluetoothValidation),
|
resolver: classValidatorResolver(BluetoothValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -35,22 +35,22 @@ export const Bluetooth = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setConfig(
|
connection.setConfig({
|
||||||
{
|
config: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "bluetooth",
|
oneofKind: "bluetooth",
|
||||||
bluetooth: data,
|
bluetooth: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Bluetooth Config, Restarting Node",
|
success: "Saved Bluetooth Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ export const Bluetooth = (): JSX.Element => {
|
|||||||
const pairingMode = useWatch({
|
const pairingMode = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "mode",
|
name: "mode",
|
||||||
defaultValue: Protobuf.Config_BluetoothConfig_PairingMode.RANDOM_PIN,
|
defaultValue: Protobuf.Config_BluetoothConfig_PairingMode.RANDOM_PIN
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -98,7 +98,7 @@ export const Bluetooth = (): JSX.Element => {
|
|||||||
description="Pin to use when pairing"
|
description="Pin to use when pairing"
|
||||||
type="number"
|
type="number"
|
||||||
{...register("fixedPin", {
|
{...register("fixedPin", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
|
import { Input } from "@app/components/form/Input.js";
|
||||||
import { Select } from "@app/components/form/Select.js";
|
import { Select } from "@app/components/form/Select.js";
|
||||||
import { Toggle } from "@app/components/form/Toggle.js";
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { DeviceValidation } from "@app/validation/config/device.js";
|
import { DeviceValidation } from "@app/validation/config/device.js";
|
||||||
@@ -20,10 +21,10 @@ export const Device = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
control,
|
control,
|
||||||
reset,
|
reset
|
||||||
} = useForm<DeviceValidation>({
|
} = useForm<DeviceValidation>({
|
||||||
defaultValues: config.device,
|
defaultValues: config.device,
|
||||||
resolver: classValidatorResolver(DeviceValidation),
|
resolver: classValidatorResolver(DeviceValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,22 +34,22 @@ export const Device = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setConfig(
|
connection.setConfig({
|
||||||
{
|
config: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "device",
|
oneofKind: "device",
|
||||||
device: data,
|
device: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Device Config, Restarting Node",
|
success: "Saved Device Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -93,6 +94,20 @@ export const Device = (): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
label="Button Pin"
|
||||||
|
description="Button pin override"
|
||||||
|
type="number"
|
||||||
|
error={errors.buttonGpio?.message}
|
||||||
|
{...register("buttonGpio", { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Buzzer Pin"
|
||||||
|
description="Buzzer pin override"
|
||||||
|
type="number"
|
||||||
|
error={errors.buzzerGpio?.message}
|
||||||
|
{...register("buzzerGpio", { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ export const Display = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<Protobuf.Config_DisplayConfig>({
|
} = useForm<Protobuf.Config_DisplayConfig>({
|
||||||
defaultValues: config.display,
|
defaultValues: config.display,
|
||||||
resolver: classValidatorResolver(DisplayValidation),
|
resolver: classValidatorResolver(DisplayValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,22 +34,22 @@ export const Display = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setConfig(
|
connection.setConfig({
|
||||||
{
|
config: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "display",
|
oneofKind: "display",
|
||||||
display: data,
|
display: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Display Config, Restarting Node",
|
success: "Saved Display Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -115,6 +115,13 @@ export const Display = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_DisplayConfig_DisplayUnits)}
|
{renderOptions(Protobuf.Config_DisplayConfig_DisplayUnits)}
|
||||||
</Select>
|
</Select>
|
||||||
|
<Select
|
||||||
|
label="OLED Type"
|
||||||
|
description="Type of OLED screen attached to the device"
|
||||||
|
{...register("oled", { valueAsNumber: true })}
|
||||||
|
>
|
||||||
|
{renderOptions(Protobuf.Config_DisplayConfig_OledType)}
|
||||||
|
</Select>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,16 +23,16 @@ export const LoRa = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
control,
|
control,
|
||||||
reset,
|
reset
|
||||||
} = useForm<LoRaValidation>({
|
} = useForm<LoRaValidation>({
|
||||||
defaultValues: config.lora,
|
defaultValues: config.lora,
|
||||||
resolver: classValidatorResolver(LoRaValidation),
|
resolver: classValidatorResolver(LoRaValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
const usePreset = useWatch({
|
const usePreset = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "usePreset",
|
name: "usePreset",
|
||||||
defaultValue: true,
|
defaultValue: true
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,22 +42,22 @@ export const LoRa = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setConfig(
|
connection.setConfig({
|
||||||
{
|
config: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "lora",
|
oneofKind: "lora",
|
||||||
lora: data,
|
lora: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved LoRa Config, Restarting Node",
|
success: "Saved LoRa Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export const LoRa = (): JSX.Element => {
|
|||||||
suffix="MHz"
|
suffix="MHz"
|
||||||
error={errors.bandwidth?.message}
|
error={errors.bandwidth?.message}
|
||||||
{...register("bandwidth", {
|
{...register("bandwidth", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
disabled={usePreset}
|
disabled={usePreset}
|
||||||
/>
|
/>
|
||||||
@@ -110,7 +110,7 @@ export const LoRa = (): JSX.Element => {
|
|||||||
suffix="CPS"
|
suffix="CPS"
|
||||||
error={errors.spreadFactor?.message}
|
error={errors.spreadFactor?.message}
|
||||||
{...register("spreadFactor", {
|
{...register("spreadFactor", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
disabled={usePreset}
|
disabled={usePreset}
|
||||||
/>
|
/>
|
||||||
@@ -120,7 +120,7 @@ export const LoRa = (): JSX.Element => {
|
|||||||
type="number"
|
type="number"
|
||||||
error={errors.codingRate?.message}
|
error={errors.codingRate?.message}
|
||||||
{...register("codingRate", {
|
{...register("codingRate", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
disabled={usePreset}
|
disabled={usePreset}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { toast } from "react-hot-toast";
|
|||||||
|
|
||||||
import { FormSection } from "@app/components/form/FormSection.js";
|
import { FormSection } from "@app/components/form/FormSection.js";
|
||||||
import { Input } from "@app/components/form/Input.js";
|
import { Input } from "@app/components/form/Input.js";
|
||||||
|
import { IPAddress } from "@app/components/form/IPAddress.js";
|
||||||
import { Select } from "@app/components/form/Select.js";
|
import { Select } from "@app/components/form/Select.js";
|
||||||
import { Toggle } from "@app/components/form/Toggle.js";
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
import { renderOptions } from "@app/core/utils/selectEnumOptions.js";
|
import { renderOptions } from "@app/core/utils/selectEnumOptions.js";
|
||||||
import { NetworkValidation } from "@app/validation/config/network.js";
|
import { NetworkValidation } from "@app/validation/config/network.js";
|
||||||
import { Form } from "@components/form/Form";
|
import { Form } from "@components/form/Form";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
|
import { ErrorMessage } from "@hookform/error-message";
|
||||||
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
@@ -22,22 +24,22 @@ export const Network = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
control,
|
control,
|
||||||
reset,
|
reset
|
||||||
} = useForm<NetworkValidation>({
|
} = useForm<NetworkValidation>({
|
||||||
defaultValues: config.network,
|
defaultValues: config.network,
|
||||||
resolver: classValidatorResolver(NetworkValidation),
|
resolver: classValidatorResolver(NetworkValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
const wifiEnabled = useWatch({
|
const wifiEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "wifiEnabled",
|
name: "wifiEnabled",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const ethEnabled = useWatch({
|
const ethEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "ethEnabled",
|
name: "ethEnabled",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,24 +47,32 @@ export const Network = (): JSX.Element => {
|
|||||||
}, [reset, config.network]);
|
}, [reset, config.network]);
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
if (connection) {
|
if (connection) {
|
||||||
|
const tmp = Protobuf.Config_NetworkConfig.create({
|
||||||
|
ethEnabled: true,
|
||||||
|
ethMode: Protobuf.Config_NetworkConfig_EthMode.DHCP
|
||||||
|
});
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setConfig(
|
connection
|
||||||
{
|
.setConfig({
|
||||||
|
config: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "network",
|
oneofKind: "network",
|
||||||
network: data,
|
network: tmp
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
})
|
||||||
|
.catch((e) => console.log(e)),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Network Config, Restarting Node",
|
success: "Saved Network Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -76,6 +86,19 @@ export const Network = (): JSX.Element => {
|
|||||||
dirty={isDirty}
|
dirty={isDirty}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
|
<ErrorMessage errors={errors} name="wifiEnabled" />
|
||||||
|
<ErrorMessage errors={errors} name="wifiMode" />
|
||||||
|
<ErrorMessage errors={errors} name="wifiSsid" />
|
||||||
|
<ErrorMessage errors={errors} name="wifiPsk" />
|
||||||
|
<ErrorMessage errors={errors} name="ntpServer" />
|
||||||
|
<ErrorMessage errors={errors} name="ethEnabled" />
|
||||||
|
<ErrorMessage errors={errors} name="ethMode" />
|
||||||
|
<ErrorMessage errors={errors} name="ethConfig" />
|
||||||
|
<ErrorMessage errors={errors} name="ip" />
|
||||||
|
<ErrorMessage errors={errors} name="gateway" />
|
||||||
|
<ErrorMessage errors={errors} name="subnet" />
|
||||||
|
<ErrorMessage errors={errors} name="dns" />
|
||||||
|
|
||||||
<FormSection title="WiFi Config">
|
<FormSection title="WiFi Config">
|
||||||
<Controller
|
<Controller
|
||||||
name="wifiEnabled"
|
name="wifiEnabled"
|
||||||
@@ -83,26 +106,18 @@ export const Network = (): JSX.Element => {
|
|||||||
render={({ field: { value, ...rest } }) => (
|
render={({ field: { value, ...rest } }) => (
|
||||||
<Toggle
|
<Toggle
|
||||||
label="WiFi Enabled"
|
label="WiFi Enabled"
|
||||||
description="Enable or disbale the WiFi radio"
|
description="Enable or disable the WiFi radio"
|
||||||
checked={value}
|
checked={value}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Select
|
|
||||||
label="WiFi Mode"
|
|
||||||
description="How the WiFi radio should be used"
|
|
||||||
disabled={!wifiEnabled}
|
|
||||||
{...register("wifiMode", { valueAsNumber: true })}
|
|
||||||
>
|
|
||||||
{renderOptions(Protobuf.Config_NetworkConfig_WiFiMode)}
|
|
||||||
</Select>
|
|
||||||
<Input
|
<Input
|
||||||
label="SSID"
|
label="SSID"
|
||||||
description="Network name"
|
description="Network name"
|
||||||
error={errors.wifiSsid?.message}
|
error={errors.wifiSsid?.message}
|
||||||
disabled={!wifiEnabled}
|
disabled={!wifiEnabled}
|
||||||
{...register("wifiSsid")}
|
{...register("wifiSsid", { disabled: !wifiEnabled })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="PSK"
|
label="PSK"
|
||||||
@@ -110,7 +125,7 @@ export const Network = (): JSX.Element => {
|
|||||||
description="Network password"
|
description="Network password"
|
||||||
error={errors.wifiPsk?.message}
|
error={errors.wifiPsk?.message}
|
||||||
disabled={!wifiEnabled}
|
disabled={!wifiEnabled}
|
||||||
{...register("wifiPsk")}
|
{...register("wifiPsk", { disabled: !wifiEnabled })}
|
||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection title="Ethernet Config">
|
<FormSection title="Ethernet Config">
|
||||||
@@ -130,35 +145,43 @@ export const Network = (): JSX.Element => {
|
|||||||
label="Ethernet Mode"
|
label="Ethernet Mode"
|
||||||
description="Address assignment selection"
|
description="Address assignment selection"
|
||||||
disabled={!ethEnabled}
|
disabled={!ethEnabled}
|
||||||
{...register("ethMode", { valueAsNumber: true })}
|
{...register("ethMode", {
|
||||||
|
valueAsNumber: true,
|
||||||
|
disabled: !ethEnabled
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{renderOptions(Protobuf.Config_NetworkConfig_EthMode)}
|
{renderOptions(Protobuf.Config_NetworkConfig_EthMode)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<FormSection title="IP Config">
|
<FormSection title="IP Config">
|
||||||
|
<IPAddress label="IP" description="IP Address" />
|
||||||
<Input
|
<Input
|
||||||
label="IP"
|
label="IP"
|
||||||
|
type="number"
|
||||||
description="IP Address"
|
description="IP Address"
|
||||||
error={errors.ethConfig?.ip?.message}
|
error={errors.ipv4Config?.ip?.message}
|
||||||
{...register("ethConfig.ip")}
|
{...register("ipv4Config.ip", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Gateway"
|
label="Gateway"
|
||||||
|
type="number"
|
||||||
description="Default Gateway"
|
description="Default Gateway"
|
||||||
error={errors.ethConfig?.gateway?.message}
|
error={errors.ipv4Config?.gateway?.message}
|
||||||
{...register("ethConfig.gateway")}
|
{...register("ipv4Config.gateway", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Subnet"
|
label="Subnet"
|
||||||
|
type="number"
|
||||||
description="Subnet Mask"
|
description="Subnet Mask"
|
||||||
error={errors.ethConfig?.subnet?.message}
|
error={errors.ipv4Config?.subnet?.message}
|
||||||
{...register("ethConfig.subnet")}
|
{...register("ipv4Config.subnet", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="DNS"
|
label="DNS"
|
||||||
|
type="number"
|
||||||
description="DNS Server"
|
description="DNS Server"
|
||||||
error={errors.ethConfig?.dns?.message}
|
error={errors.ipv4Config?.dns?.message}
|
||||||
{...register("ethConfig.dns")}
|
{...register("ipv4Config.dns", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect } from "react";
|
|||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
|
import { BitwiseSelect } from "@app/components/form/BitwiseSelect.js";
|
||||||
import { FormSection } from "@app/components/form/FormSection.js";
|
import { FormSection } from "@app/components/form/FormSection.js";
|
||||||
import { Input } from "@app/components/form/Input.js";
|
import { Input } from "@app/components/form/Input.js";
|
||||||
import { Toggle } from "@app/components/form/Toggle.js";
|
import { Toggle } from "@app/components/form/Toggle.js";
|
||||||
@@ -23,21 +24,21 @@ export const Position = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<PositionValidation>({
|
} = useForm<PositionValidation>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fixedAlt: myNode?.data.position?.altitude,
|
fixedAlt: myNode?.data.position?.altitude,
|
||||||
fixedLat: (myNode?.data.position?.latitudeI ?? 0) / 1e7,
|
fixedLat: (myNode?.data.position?.latitudeI ?? 0) / 1e7,
|
||||||
fixedLng: (myNode?.data.position?.longitudeI ?? 0) / 1e7,
|
fixedLng: (myNode?.data.position?.longitudeI ?? 0) / 1e7,
|
||||||
...config.position,
|
...config.position
|
||||||
},
|
},
|
||||||
resolver: classValidatorResolver(PositionValidation),
|
resolver: classValidatorResolver(PositionValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixedPositionEnabled = useWatch({
|
const fixedPositionEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "fixedPosition",
|
name: "fixedPosition",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,7 +46,7 @@ export const Position = (): JSX.Element => {
|
|||||||
fixedAlt: myNode?.data.position?.altitude,
|
fixedAlt: myNode?.data.position?.altitude,
|
||||||
fixedLat: (myNode?.data.position?.latitudeI ?? 0) / 1e7,
|
fixedLat: (myNode?.data.position?.latitudeI ?? 0) / 1e7,
|
||||||
fixedLng: (myNode?.data.position?.longitudeI ?? 0) / 1e7,
|
fixedLng: (myNode?.data.position?.longitudeI ?? 0) / 1e7,
|
||||||
...config.position,
|
...config.position
|
||||||
});
|
});
|
||||||
}, [reset, config.position, myNode?.data.position]);
|
}, [reset, config.position, myNode?.data.position]);
|
||||||
|
|
||||||
@@ -59,49 +60,41 @@ export const Position = (): JSX.Element => {
|
|||||||
|
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.sendPacket(
|
connection.setPosition({
|
||||||
Protobuf.Position.toBinary(
|
position: Protobuf.Position.create({
|
||||||
Protobuf.Position.create({
|
|
||||||
altitude: fixedAlt,
|
altitude: fixedAlt,
|
||||||
latitudeI: fixedLat * 1e7,
|
latitudeI: fixedLat * 1e7,
|
||||||
longitudeI: fixedLng * 1e7,
|
longitudeI: fixedLng * 1e7
|
||||||
})
|
}),
|
||||||
),
|
callback: async () => {
|
||||||
Protobuf.PortNum.POSITION_APP,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Channel",
|
success: "Saved Channel",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (configHasChanged) {
|
if (configHasChanged) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setConfig(
|
connection.setConfig({
|
||||||
{
|
config: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "position",
|
oneofKind: "position",
|
||||||
position: rest,
|
position: rest
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Position Config, Restarting Node",
|
success: "Saved Position Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -201,94 +194,65 @@ export const Position = (): JSX.Element => {
|
|||||||
error={errors.gpsAttemptTime?.message}
|
error={errors.gpsAttemptTime?.message}
|
||||||
{...register("gpsAttemptTime", { valueAsNumber: true })}
|
{...register("gpsAttemptTime", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
{/* <Controller
|
<Controller
|
||||||
name="positionFlags"
|
name="positionFlags"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState }): JSX.Element => {
|
render={({ field, fieldState }): JSX.Element => {
|
||||||
const { value, onChange, ...rest } = field;
|
const { value, onChange, ...rest } = field;
|
||||||
const { error } = fieldState;
|
const { error } = fieldState;
|
||||||
const options = Object.entries(
|
// const options = Object.entries(
|
||||||
Protobuf.Config_PositionConfig_PositionFlags
|
// Protobuf.Config_PositionConfig_PositionFlags
|
||||||
)
|
// )
|
||||||
.filter((value) => typeof value[1] !== "number")
|
// .filter((value) => typeof value[1] !== "number")
|
||||||
.filter(
|
// .filter(
|
||||||
(value) =>
|
// (value) =>
|
||||||
parseInt(value[0]) !==
|
// parseInt(value[0]) !==
|
||||||
Protobuf.Config_PositionConfig_PositionFlags.UNSET
|
// Protobuf.Config_PositionConfig_PositionFlags.UNSET
|
||||||
)
|
// )
|
||||||
.map((value) => {
|
// .map((value) => {
|
||||||
return {
|
// return {
|
||||||
value: parseInt(value[0]),
|
// value: parseInt(value[0]),
|
||||||
label: value[1].toString().replace("POS_", "").toLowerCase(),
|
// label: value[1].toString().replace("POS_", "").toLowerCase(),
|
||||||
};
|
// };
|
||||||
});
|
// });
|
||||||
|
|
||||||
const selected = bitwiseDecode(
|
// const selected = bitwiseDecode(
|
||||||
value,
|
// value,
|
||||||
Protobuf.Config_PositionConfig_PositionFlags
|
// Protobuf.Config_PositionConfig_PositionFlags
|
||||||
).map((flag) =>
|
// ).map((flag) =>
|
||||||
Protobuf.Config_PositionConfig_PositionFlags[flag]
|
// Protobuf.Config_PositionConfig_PositionFlags[flag]
|
||||||
.replace("POS_", "")
|
// .replace("POS_", "")
|
||||||
.toLowerCase()
|
// .toLowerCase()
|
||||||
);
|
// );
|
||||||
// onChange={(e: { value: number; label: string }[]): void =>
|
// onChange={(e: { value: number; label: string }[]): void =>
|
||||||
// onChange(bitwiseEncode(e.map((v) => v.value)))
|
// onChange(bitwiseEncode(e.map((v) => v.value)))
|
||||||
// }
|
// }
|
||||||
return (
|
return (
|
||||||
<FormField
|
<BitwiseSelect
|
||||||
label="Position Flags"
|
label="Position Flags"
|
||||||
description="Description"
|
description="Description"
|
||||||
isInvalid={!!errors.positionFlags?.message}
|
error={error?.message}
|
||||||
validationMessage={errors.positionFlags?.message}
|
selected={value}
|
||||||
>
|
decodeEnun={Protobuf.Config_PositionConfig_PositionFlags}
|
||||||
<SelectMenu
|
onChange={onChange}
|
||||||
isMultiSelect
|
/>
|
||||||
title="Select multiple names"
|
|
||||||
options={options}
|
|
||||||
selected={selected}
|
|
||||||
// onSelect={(item) => {
|
|
||||||
// const selected = [...selectedItemsState, item.value]
|
|
||||||
// const selectedItems = selected
|
|
||||||
// const selectedItemsLength = selectedItems.length
|
|
||||||
// let selectedNames = ''
|
|
||||||
// if (selectedItemsLength === 0) {
|
|
||||||
// selectedNames = ''
|
|
||||||
// } else if (selectedItemsLength === 1) {
|
|
||||||
// selectedNames = selectedItems.toString()
|
|
||||||
// } else if (selectedItemsLength > 1) {
|
|
||||||
// selectedNames = selectedItemsLength.toString() + ' selected...'
|
|
||||||
// }
|
|
||||||
// setSelectedItems(selectedItems)
|
|
||||||
// setSelectedItemNames(selectedNames)
|
|
||||||
// }}
|
|
||||||
// onDeselect={(item) => {
|
|
||||||
// const deselectedItemIndex = selectedItemsState.indexOf(item.value)
|
|
||||||
// const selectedItems = selectedItemsState.filter((_item, i) => i !== deselectedItemIndex)
|
|
||||||
// const selectedItemsLength = selectedItems.length
|
|
||||||
// let selectedNames = ''
|
|
||||||
// if (selectedItemsLength === 0) {
|
|
||||||
// selectedNames = ''
|
|
||||||
// } else if (selectedItemsLength === 1) {
|
|
||||||
// selectedNames = selectedItems.toString()
|
|
||||||
// } else if (selectedItemsLength > 1) {
|
|
||||||
// selectedNames = selectedItemsLength.toString() + ' selected...'
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setSelectedItems(selectedItems)
|
|
||||||
// setSelectedItemNames(selectedNames)
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
<Button>
|
|
||||||
{selected.map(
|
|
||||||
(item, index) =>
|
|
||||||
`${item}${index !== selected.length - 1 ? ", " : ""}`
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</SelectMenu>
|
|
||||||
</FormField>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/> */}
|
/>
|
||||||
|
<Input
|
||||||
|
label="RX Pin"
|
||||||
|
description="GPS Module RX pin override"
|
||||||
|
type="number"
|
||||||
|
error={errors.rxGpio?.message}
|
||||||
|
{...register("rxGpio", { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="TX Pin"
|
||||||
|
description="GPS Module TX pin override"
|
||||||
|
type="number"
|
||||||
|
error={errors.txGpio?.message}
|
||||||
|
{...register("txGpio", { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ export const Power = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<PowerValidation>({
|
} = useForm<PowerValidation>({
|
||||||
defaultValues: config.power,
|
defaultValues: config.power,
|
||||||
resolver: classValidatorResolver(PowerValidation),
|
resolver: classValidatorResolver(PowerValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -32,22 +32,22 @@ export const Power = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setConfig(
|
connection.setConfig({
|
||||||
{
|
config: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "power",
|
oneofKind: "power",
|
||||||
power: data,
|
power: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Power Config, Restarting Node",
|
success: "Saved Power Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,31 +25,34 @@ export const User = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<UserValidation>({
|
} = useForm<UserValidation>({
|
||||||
defaultValues: myNode?.data.user,
|
defaultValues: myNode?.data.user,
|
||||||
resolver: classValidatorResolver(UserValidation),
|
resolver: classValidatorResolver(UserValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset({
|
reset({
|
||||||
longName: myNode?.data.user?.longName,
|
longName: myNode?.data.user?.longName,
|
||||||
shortName: myNode?.data.user?.shortName,
|
shortName: myNode?.data.user?.shortName,
|
||||||
isLicensed: myNode?.data.user?.isLicensed,
|
isLicensed: myNode?.data.user?.isLicensed
|
||||||
});
|
});
|
||||||
}, [reset, myNode]);
|
}, [reset, myNode]);
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection && myNode?.data.user) {
|
if (connection && myNode?.data.user) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setOwner({ ...myNode.data.user, ...data }, async () => {
|
connection.setOwner({
|
||||||
|
owner: { ...myNode.data.user, ...data },
|
||||||
|
callback: async () => {
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved User, Restarting Node",
|
success: "Saved User, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -63,7 +66,7 @@ export const User = (): JSX.Element => {
|
|||||||
reset({
|
reset({
|
||||||
longName: myNode?.data.user?.longName,
|
longName: myNode?.data.user?.longName,
|
||||||
shortName: myNode?.data.user?.shortName,
|
shortName: myNode?.data.user?.shortName,
|
||||||
isLicensed: myNode?.data.user?.isLicensed,
|
isLicensed: myNode?.data.user?.isLicensed
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
dirty={isDirty}
|
dirty={isDirty}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const BLE = (): JSX.Element => {
|
|||||||
setSelectedDevice(id);
|
setSelectedDevice(id);
|
||||||
const connection = new IBLEConnection(id);
|
const connection = new IBLEConnection(id);
|
||||||
await connection.connect({
|
await connection.connect({
|
||||||
device: BLEDevice,
|
device: BLEDevice
|
||||||
});
|
});
|
||||||
device.addConnection(connection);
|
device.addConnection(connection);
|
||||||
subscribeAll(device, connection);
|
subscribeAll(device, connection);
|
||||||
@@ -58,7 +58,7 @@ export const BLE = (): JSX.Element => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
void navigator.bluetooth
|
void navigator.bluetooth
|
||||||
.requestDevice({
|
.requestDevice({
|
||||||
filters: [{ services: [Constants.serviceUUID] }],
|
filters: [{ services: [Constants.serviceUUID] }]
|
||||||
})
|
})
|
||||||
.then((device) => {
|
.then((device) => {
|
||||||
const exists = bleDevices.findIndex((d) => d.id === device.id);
|
const exists = bleDevices.findIndex((d) => d.id === device.id);
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ export const HTTP = (): JSX.Element => {
|
|||||||
}>({
|
}>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: "meshtastic.local",
|
ip: "meshtastic.local",
|
||||||
tls: location.protocol === "https:",
|
tls: location.protocol === "https:"
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const TLSEnabled = useWatch({
|
const TLSEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "tls",
|
name: "tls",
|
||||||
defaultValue: location.protocol === "https:",
|
defaultValue: location.protocol === "https:"
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
@@ -40,7 +40,7 @@ export const HTTP = (): JSX.Element => {
|
|||||||
void connection.connect({
|
void connection.connect({
|
||||||
address: data.ip,
|
address: data.ip,
|
||||||
fetchInterval: 2000,
|
fetchInterval: 2000,
|
||||||
tls: data.tls,
|
tls: data.tls
|
||||||
});
|
});
|
||||||
device.addConnection(connection);
|
device.addConnection(connection);
|
||||||
subscribeAll(device, connection);
|
subscribeAll(device, connection);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
.connect({
|
.connect({
|
||||||
port,
|
port,
|
||||||
baudRate: undefined,
|
baudRate: undefined,
|
||||||
concurrentLogOutput: true,
|
concurrentLogOutput: true
|
||||||
})
|
})
|
||||||
.catch((e: Error) => console.log(`Unable to Connect: ${e.message}`));
|
.catch((e: Error) => console.log(`Unable to Connect: ${e.message}`));
|
||||||
device.addConnection(connection);
|
device.addConnection(connection);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { AllMessageTypes } from "@core/stores/deviceStore.js";
|
|||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
EllipsisHorizontalCircleIcon,
|
EllipsisHorizontalCircleIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export interface MessageProps {
|
|||||||
export const Message = ({
|
export const Message = ({
|
||||||
lastMsgSameUser,
|
lastMsgSameUser,
|
||||||
message,
|
message,
|
||||||
sender,
|
sender
|
||||||
}: MessageProps): JSX.Element => {
|
}: MessageProps): JSX.Element => {
|
||||||
const { setPeerInfoOpen, setActivePeer, connection } = useDevice();
|
const { setPeerInfoOpen, setActivePeer, connection } = useDevice();
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export const Message = ({
|
|||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{new Date(message.packet.rxTime).toLocaleTimeString(undefined, {
|
{new Date(message.packet.rxTime).toLocaleTimeString(undefined, {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit"
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,21 +20,20 @@ export const MessageInput = ({ channel }: MessageInputProps): JSX.Element => {
|
|||||||
message: string;
|
message: string;
|
||||||
}>({
|
}>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
message: "",
|
message: ""
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
void connection?.sendText(
|
void connection?.sendText({
|
||||||
data.message,
|
text: data.message,
|
||||||
undefined,
|
wantAck: true,
|
||||||
true,
|
channel: channel.config.index as Types.ChannelNumber,
|
||||||
channel.config.index as Types.ChannelNumber,
|
callback: (id) => {
|
||||||
(id) => {
|
|
||||||
ackMessage(channel.config.index, id);
|
ackMessage(channel.config.index, id);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Protobuf } from "@meshtastic/meshtasticjs";
|
|||||||
enum LocationType {
|
enum LocationType {
|
||||||
MGRS,
|
MGRS,
|
||||||
LatLng,
|
LatLng,
|
||||||
DecimalDegrees,
|
DecimalDegrees
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NewLocationMessage = (): JSX.Element => {
|
export const NewLocationMessage = (): JSX.Element => {
|
||||||
@@ -31,14 +31,15 @@ export const NewLocationMessage = (): JSX.Element => {
|
|||||||
<Input label="Coordinates" />
|
<Input label="Coordinates" />
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void connection?.sendWaypoint(
|
void connection?.sendWaypoint({
|
||||||
Protobuf.Waypoint.create({
|
waypoint: Protobuf.Waypoint.create({
|
||||||
latitudeI: Math.floor(3.89103 * 1e7),
|
latitudeI: Math.floor(3.89103 * 1e7),
|
||||||
longitudeI: Math.floor(105.87005 * 1e7),
|
longitudeI: Math.floor(105.87005 * 1e7),
|
||||||
name: "TEST",
|
name: "TEST",
|
||||||
description: "This is a description",
|
description: "This is a description"
|
||||||
})
|
}),
|
||||||
);
|
destination: "broadcast"
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface WaypointMessageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const WaypointMessage = ({
|
export const WaypointMessage = ({
|
||||||
waypointID,
|
waypointID
|
||||||
}: WaypointMessageProps): JSX.Element => {
|
}: WaypointMessageProps): JSX.Element => {
|
||||||
const { waypoints } = useDevice();
|
const { waypoints } = useDevice();
|
||||||
const waypoint = waypoints.find((wp) => wp.id === waypointID);
|
const waypoint = waypoints.find((wp) => wp.id === waypointID);
|
||||||
|
|||||||
@@ -21,16 +21,16 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<CannedMessageValidation>({
|
} = useForm<CannedMessageValidation>({
|
||||||
defaultValues: moduleConfig.cannedMessage,
|
defaultValues: moduleConfig.cannedMessage,
|
||||||
resolver: classValidatorResolver(CannedMessageValidation),
|
resolver: classValidatorResolver(CannedMessageValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
const moduleEnabled = useWatch({
|
const moduleEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "rotary1Enabled",
|
name: "rotary1Enabled",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,22 +40,22 @@ export const CannedMessage = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setModuleConfig(
|
connection.setModuleConfig({
|
||||||
{
|
moduleConfig: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "cannedMessage",
|
oneofKind: "cannedMessage",
|
||||||
cannedMessage: data,
|
cannedMessage: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Canned Message Config, Restarting Node",
|
success: "Saved Canned Message Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ export const ExternalNotification = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<ExternalNotificationValidation>({
|
} = useForm<ExternalNotificationValidation>({
|
||||||
defaultValues: moduleConfig.externalNotification,
|
defaultValues: moduleConfig.externalNotification,
|
||||||
resolver: classValidatorResolver(ExternalNotificationValidation),
|
resolver: classValidatorResolver(ExternalNotificationValidation)
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(moduleConfig.externalNotification);
|
reset(moduleConfig.externalNotification);
|
||||||
@@ -30,22 +30,22 @@ export const ExternalNotification = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setModuleConfig(
|
connection.setModuleConfig({
|
||||||
{
|
moduleConfig: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "externalNotification",
|
oneofKind: "externalNotification",
|
||||||
externalNotification: data,
|
externalNotification: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved External Notification Config, Restarting Node",
|
success: "Saved External Notification Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ export const ExternalNotification = (): JSX.Element => {
|
|||||||
const moduleEnabled = useWatch({
|
const moduleEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "enabled",
|
name: "enabled",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,7 +84,7 @@ export const ExternalNotification = (): JSX.Element => {
|
|||||||
suffix="ms"
|
suffix="ms"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("outputMs", {
|
{...register("outputMs", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -93,7 +93,7 @@ export const ExternalNotification = (): JSX.Element => {
|
|||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("output", {
|
{...register("output", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -132,6 +132,18 @@ export const ExternalNotification = (): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="usePwm"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, ...rest } }) => (
|
||||||
|
<Toggle
|
||||||
|
label="Use PWM"
|
||||||
|
description="Description"
|
||||||
|
checked={value}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ export const MQTT = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<MQTTValidation>({
|
} = useForm<MQTTValidation>({
|
||||||
defaultValues: moduleConfig.mqtt,
|
defaultValues: moduleConfig.mqtt,
|
||||||
resolver: classValidatorResolver(MQTTValidation),
|
resolver: classValidatorResolver(MQTTValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
const moduleEnabled = useWatch({
|
const moduleEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "enabled",
|
name: "enabled",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -37,22 +37,22 @@ export const MQTT = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setModuleConfig(
|
connection.setModuleConfig({
|
||||||
{
|
moduleConfig: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "mqtt",
|
oneofKind: "mqtt",
|
||||||
mqtt: data,
|
mqtt: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved MQTT Config, Restarting Node",
|
success: "Saved MQTT Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ export const RangeTest = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<RangeTestValidation>({
|
} = useForm<RangeTestValidation>({
|
||||||
defaultValues: moduleConfig.rangeTest,
|
defaultValues: moduleConfig.rangeTest,
|
||||||
resolver: classValidatorResolver(RangeTestValidation),
|
resolver: classValidatorResolver(RangeTestValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,22 +31,22 @@ export const RangeTest = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setModuleConfig(
|
connection.setModuleConfig({
|
||||||
{
|
moduleConfig: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "rangeTest",
|
oneofKind: "rangeTest",
|
||||||
rangeTest: data,
|
rangeTest: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Range Test Config, Restarting Node",
|
success: "Saved Range Test Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export const RangeTest = (): JSX.Element => {
|
|||||||
const moduleEnabled = useWatch({
|
const moduleEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "enabled",
|
name: "enabled",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,7 +85,7 @@ export const RangeTest = (): JSX.Element => {
|
|||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
suffix="Seconds"
|
suffix="Seconds"
|
||||||
{...register("sender", {
|
{...register("sender", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ export const Serial = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<SerialValidation>({
|
} = useForm<SerialValidation>({
|
||||||
defaultValues: moduleConfig.serial,
|
defaultValues: moduleConfig.serial,
|
||||||
resolver: classValidatorResolver(SerialValidation),
|
resolver: classValidatorResolver(SerialValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,22 +31,22 @@ export const Serial = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setModuleConfig(
|
connection.setModuleConfig({
|
||||||
{
|
moduleConfig: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "serial",
|
oneofKind: "serial",
|
||||||
serial: data,
|
serial: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Serial Config, Restarting Node",
|
success: "Saved Serial Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
const moduleEnabled = useWatch({
|
const moduleEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "enabled",
|
name: "enabled",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,7 +96,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("rxd", {
|
{...register("rxd", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -105,7 +105,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("txd", {
|
{...register("txd", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -114,7 +114,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("baud", {
|
{...register("baud", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -123,7 +123,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("timeout", {
|
{...register("timeout", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -132,7 +132,7 @@ export const Serial = (): JSX.Element => {
|
|||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("mode", {
|
{...register("mode", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ export const StoreForward = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<StoreForwardValidation>({
|
} = useForm<StoreForwardValidation>({
|
||||||
defaultValues: moduleConfig.storeForward,
|
defaultValues: moduleConfig.storeForward,
|
||||||
resolver: classValidatorResolver(StoreForwardValidation),
|
resolver: classValidatorResolver(StoreForwardValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,22 +31,22 @@ export const StoreForward = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setModuleConfig(
|
connection.setModuleConfig({
|
||||||
{
|
moduleConfig: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "storeForward",
|
oneofKind: "storeForward",
|
||||||
storeForward: data,
|
storeForward: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Store & Forward Config, Restarting Node",
|
success: "Saved Store & Forward Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export const StoreForward = (): JSX.Element => {
|
|||||||
const moduleEnabled = useWatch({
|
const moduleEnabled = useWatch({
|
||||||
control,
|
control,
|
||||||
name: "enabled",
|
name: "enabled",
|
||||||
defaultValue: false,
|
defaultValue: false
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -97,7 +97,7 @@ export const StoreForward = (): JSX.Element => {
|
|||||||
suffix="Records"
|
suffix="Records"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("records", {
|
{...register("records", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -106,7 +106,7 @@ export const StoreForward = (): JSX.Element => {
|
|||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("historyReturnMax", {
|
{...register("historyReturnMax", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -115,7 +115,7 @@ export const StoreForward = (): JSX.Element => {
|
|||||||
description="Max transmit power in dBm"
|
description="Max transmit power in dBm"
|
||||||
disabled={!moduleEnabled}
|
disabled={!moduleEnabled}
|
||||||
{...register("historyReturnWindow", {
|
{...register("historyReturnWindow", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ export const Telemetry = (): JSX.Element => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isDirty },
|
formState: { errors, isDirty },
|
||||||
reset,
|
reset,
|
||||||
control,
|
control
|
||||||
} = useForm<TelemetryValidation>({
|
} = useForm<TelemetryValidation>({
|
||||||
defaultValues: moduleConfig.telemetry,
|
defaultValues: moduleConfig.telemetry,
|
||||||
resolver: classValidatorResolver(TelemetryValidation),
|
resolver: classValidatorResolver(TelemetryValidation)
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,22 +31,22 @@ export const Telemetry = (): JSX.Element => {
|
|||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.setModuleConfig(
|
connection.setModuleConfig({
|
||||||
{
|
moduleConfig: {
|
||||||
payloadVariant: {
|
payloadVariant: {
|
||||||
oneofKind: "telemetry",
|
oneofKind: "telemetry",
|
||||||
telemetry: data,
|
telemetry: data
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
callback: async () => {
|
||||||
async () => {
|
|
||||||
reset({ ...data });
|
reset({ ...data });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
{
|
{
|
||||||
loading: "Saving...",
|
loading: "Saving...",
|
||||||
success: "Saved Telemetry Config, Restarting Node",
|
success: "Saved Telemetry Config, Restarting Node",
|
||||||
error: "No response received",
|
error: "No response received"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ export const Telemetry = (): JSX.Element => {
|
|||||||
suffix="Seconds"
|
suffix="Seconds"
|
||||||
type="number"
|
type="number"
|
||||||
{...register("environmentUpdateInterval", {
|
{...register("environmentUpdateInterval", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
+10
-10
@@ -10,7 +10,7 @@ import {
|
|||||||
InboxIcon,
|
InboxIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
Square3Stack3DIcon,
|
Square3Stack3DIcon,
|
||||||
UsersIcon,
|
UsersIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export const PageNav = (): JSX.Element => {
|
export const PageNav = (): JSX.Element => {
|
||||||
@@ -26,43 +26,43 @@ export const PageNav = (): JSX.Element => {
|
|||||||
{
|
{
|
||||||
name: "Messages",
|
name: "Messages",
|
||||||
icon: <InboxIcon />,
|
icon: <InboxIcon />,
|
||||||
page: "messages",
|
page: "messages"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Map",
|
name: "Map",
|
||||||
icon: <MapIcon />,
|
icon: <MapIcon />,
|
||||||
page: "map",
|
page: "map"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Extensions",
|
name: "Extensions",
|
||||||
icon: <BeakerIcon />,
|
icon: <BeakerIcon />,
|
||||||
page: "extensions",
|
page: "extensions"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Config",
|
name: "Config",
|
||||||
icon: <Cog8ToothIcon />,
|
icon: <Cog8ToothIcon />,
|
||||||
page: "config",
|
page: "config"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Channels",
|
name: "Channels",
|
||||||
icon: <Square3Stack3DIcon />,
|
icon: <Square3Stack3DIcon />,
|
||||||
page: "channels",
|
page: "channels"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Peers",
|
name: "Peers",
|
||||||
icon: <UsersIcon />,
|
icon: <UsersIcon />,
|
||||||
page: "peers",
|
page: "peers"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Info",
|
name: "Info",
|
||||||
icon: <IdentificationIcon />,
|
icon: <IdentificationIcon />,
|
||||||
page: "info",
|
page: "info"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Logs",
|
name: "Logs",
|
||||||
icon: <DocumentTextIcon />,
|
icon: <DocumentTextIcon />,
|
||||||
page: "logs",
|
page: "logs"
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export interface BatteryWidgetProps {
|
|||||||
|
|
||||||
export const BatteryWidget = ({
|
export const BatteryWidget = ({
|
||||||
batteryLevel,
|
batteryLevel,
|
||||||
voltage,
|
voltage
|
||||||
}: BatteryWidgetProps): JSX.Element => {
|
}: BatteryWidgetProps): JSX.Element => {
|
||||||
const { nodes, hardware } = useDevice();
|
const { nodes, hardware } = useDevice();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const ConfiguringWidget = (): JSX.Element => {
|
|||||||
moduleConfig,
|
moduleConfig,
|
||||||
setReady,
|
setReady,
|
||||||
nodes,
|
nodes,
|
||||||
connection,
|
connection
|
||||||
} = useDevice();
|
} = useDevice();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -32,7 +32,7 @@ export const ConfiguringWidget = (): JSX.Element => {
|
|||||||
channels,
|
channels,
|
||||||
hardware.maxChannels,
|
hardware.maxChannels,
|
||||||
hardware.myNodeNum,
|
hardware.myNodeNum,
|
||||||
setReady,
|
setReady
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,7 +80,7 @@ export interface StatusIndicatorProps {
|
|||||||
const StatusIndicator = ({
|
const StatusIndicator = ({
|
||||||
title,
|
title,
|
||||||
current,
|
current,
|
||||||
total,
|
total
|
||||||
}: StatusIndicatorProps): JSX.Element => {
|
}: StatusIndicatorProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<li className="relative">
|
<li className="relative">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const DeviceWidget = ({
|
|||||||
nodeNum,
|
nodeNum,
|
||||||
disconnected,
|
disconnected,
|
||||||
disconnect,
|
disconnect,
|
||||||
reconnect,
|
reconnect
|
||||||
}: DeviceWidgetProps): JSX.Element => {
|
}: DeviceWidgetProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Card className="relative shrink-0 flex-col">
|
<Card className="relative shrink-0 flex-col">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface NodeInfoWidgetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const NodeInfoWidget = ({
|
export const NodeInfoWidget = ({
|
||||||
hardware,
|
hardware
|
||||||
}: NodeInfoWidgetProps): JSX.Element => {
|
}: NodeInfoWidgetProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Card className="flex-col">
|
<Card className="flex-col">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type React from "react";
|
|||||||
import { useDevice } from "@app/core/providers/useDevice.js";
|
import { useDevice } from "@app/core/providers/useDevice.js";
|
||||||
import {
|
import {
|
||||||
EllipsisHorizontalIcon,
|
EllipsisHorizontalIcon,
|
||||||
UserGroupIcon,
|
UserGroupIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
bitwiseDecode,
|
||||||
|
bitwiseEncode,
|
||||||
|
enumLike
|
||||||
|
} from "@app/core/utils/bitwise.js";
|
||||||
|
import { Listbox } from "@headlessui/react";
|
||||||
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
|
import { InfoWrapper } from "./InfoWrapper.js";
|
||||||
|
|
||||||
|
export interface BitwiseSelectProps {
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
error?: string;
|
||||||
|
selected: number;
|
||||||
|
decodeEnun: enumLike;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BitwiseSelect = ({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
error,
|
||||||
|
selected,
|
||||||
|
decodeEnun,
|
||||||
|
onChange
|
||||||
|
}: BitwiseSelectProps): JSX.Element => {
|
||||||
|
const [decodedSelected, setDecodedSelected] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const options = Object.entries(decodeEnun)
|
||||||
|
.filter((value) => typeof value[1] !== "number")
|
||||||
|
.map((value) => {
|
||||||
|
return {
|
||||||
|
value: parseInt(value[0]),
|
||||||
|
label: value[1]
|
||||||
|
.toString()
|
||||||
|
.replace("POS_", "")
|
||||||
|
.toLowerCase()
|
||||||
|
.toLocaleUpperCase() //TODO: Investigate
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setDecodedSelected(
|
||||||
|
bitwiseDecode(selected, Protobuf.Config_PositionConfig_PositionFlags).map(
|
||||||
|
(flag) =>
|
||||||
|
Protobuf.Config_PositionConfig_PositionFlags[flag]
|
||||||
|
.replace("POS_", "")
|
||||||
|
.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfoWrapper label={label} description={description} error={error}>
|
||||||
|
<Listbox
|
||||||
|
value={bitwiseDecode(selected, decodeEnun)}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange(bitwiseEncode(value));
|
||||||
|
}}
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<Listbox.Button
|
||||||
|
className={`flex h-10 w-full items-center gap-2 rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500`}
|
||||||
|
>
|
||||||
|
{decodedSelected.map((option) => (
|
||||||
|
<span className="rounded-md bg-orange-300 p-1">{option}</span>
|
||||||
|
))}
|
||||||
|
</Listbox.Button>
|
||||||
|
<Listbox.Options>
|
||||||
|
{options.map((option) => (
|
||||||
|
<Listbox.Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Listbox>
|
||||||
|
</InfoWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ import { Button } from "@components/Button.js";
|
|||||||
import {
|
import {
|
||||||
ArrowUturnLeftIcon,
|
ArrowUturnLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
HomeIcon,
|
HomeIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export interface FormProps extends HTMLProps<HTMLFormElement> {
|
export interface FormProps extends HTMLProps<HTMLFormElement> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface FormSectionProps {
|
|||||||
|
|
||||||
export const FormSection = ({
|
export const FormSection = ({
|
||||||
title,
|
title,
|
||||||
children,
|
children
|
||||||
}: FormSectionProps): JSX.Element => {
|
}: FormSectionProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import React, { InputHTMLAttributes, useState } from "react";
|
||||||
|
|
||||||
|
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export interface IPAddressProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
|
action?: {
|
||||||
|
icon: JSX.Element;
|
||||||
|
action: () => void;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IPAddress = ({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
action,
|
||||||
|
error,
|
||||||
|
disabled,
|
||||||
|
...rest
|
||||||
|
}: IPAddressProps): JSX.Element => {
|
||||||
|
const [value, setValue] = useState<[number, number, number, number]>([
|
||||||
|
0, 0, 0, 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
// const getRange = (el) => {
|
||||||
|
// var cuRange, tbRange, headRange, range, dupRange, ret = {};
|
||||||
|
// if (el.setSelectionRange) {
|
||||||
|
// // standard
|
||||||
|
// ret.begin = el.selectionStart;
|
||||||
|
// ret.end = el.selectionEnd;
|
||||||
|
// ret.result = el.value.substring(ret.begin, ret.end);
|
||||||
|
// } else if (document.selection) {
|
||||||
|
// // ie
|
||||||
|
// if (el.tagName.toLowerCase() === 'input') {
|
||||||
|
// cuRange = document.selection.createRange();
|
||||||
|
// tbRange = el.createTextRange();
|
||||||
|
// tbRange.collapse(true);
|
||||||
|
// tbRange.select();
|
||||||
|
// headRange = document.selection.createRange();
|
||||||
|
// headRange.setEndPoint('EndToEnd', cuRange);
|
||||||
|
// ret.begin = headRange.text.length - cuRange.text.length;
|
||||||
|
// ret.end = headRange.text.length;
|
||||||
|
// ret.result = cuRange.text;
|
||||||
|
// cuRange.select();
|
||||||
|
// } else if (el.tagName.toLowerCase() === 'textarea') {
|
||||||
|
// range = document.selection.createRange();
|
||||||
|
// dupRange = range.duplicate();
|
||||||
|
// dupRange.moveToElementText(el);
|
||||||
|
// dupRange.setEndPoint('EndToEnd', range);
|
||||||
|
// ret.begin = dupRange.text.length - range.text.length;
|
||||||
|
// ret.end = dupRange.text.length;
|
||||||
|
// ret.result = range.text;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// el.focus();
|
||||||
|
// return ret;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const isValidIPItemValue = (val) => {
|
||||||
|
// val = parseInt(val);
|
||||||
|
// return !isNaN(val) && val >= 0 && val <= 255;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, index: number) => {
|
||||||
|
// /* 37 = ←, 39 = →, 8 = backspace, 110 or 190 = . */
|
||||||
|
// let domId = index;
|
||||||
|
// if ((event.keyCode === 37 || event.keyCode === 8) && getRange(event.target).end === 0 && index > 0) { domId = index - 1; }
|
||||||
|
// if (event.keyCode === 39 && getRange(event.target).end === event.target.value.length && index < 3) { domId = index + 1; }
|
||||||
|
// if (event.keyCode === 110 || event.keyCode === 190) {
|
||||||
|
// event.preventDefault();
|
||||||
|
// if(i < 3) {
|
||||||
|
// domId = i + 1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// this[`_input-${domId}`].focus();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// const ip = value.map(val => isNaN(val) ? '' : val).join('.');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Label */}
|
||||||
|
<label className="block text-sm font-medium text-gray-700">{label}</label>
|
||||||
|
{/* */}
|
||||||
|
<div className="relative flex gap-1 rounded-md">
|
||||||
|
{value.map((octet, index) => (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
key={index}
|
||||||
|
// ref={ref}
|
||||||
|
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm shadow-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||||
|
action ? "rounded-r-none" : ""
|
||||||
|
} ${
|
||||||
|
disabled
|
||||||
|
? "cursor-not-allowed bg-orange-50 text-orange-200"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
disabled={disabled}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{index !== 3 && <i className="text-xl">.</i>}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
{action && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={action.action}
|
||||||
|
className="relative -ml-px inline-flex items-center space-x-2 rounded-r-md bg-orange-200 px-4 py-2 text-sm font-medium hover:bg-orange-300 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
|
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{description && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500">{description}</p>
|
||||||
|
)}
|
||||||
|
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export interface InfoWrapperProps {
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
error?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfoWrapper = ({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
error,
|
||||||
|
children
|
||||||
|
}: InfoWrapperProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* Label */}
|
||||||
|
{label && (
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{/* */}
|
||||||
|
{children}
|
||||||
|
{error && (
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
|
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500">{description}</p>
|
||||||
|
)}
|
||||||
|
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,16 +3,17 @@ import { forwardRef, InputHTMLAttributes } from "react";
|
|||||||
|
|
||||||
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
import { InfoWrapper, InfoWrapperProps } from "./InfoWrapper.js";
|
||||||
label: string;
|
|
||||||
description?: string;
|
export interface InputProps
|
||||||
|
extends InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
Omit<InfoWrapperProps, "children"> {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
action?: {
|
action?: {
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
action: () => void;
|
action: () => void;
|
||||||
};
|
};
|
||||||
error?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
||||||
@@ -29,10 +30,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<InfoWrapper label={label} description={description} error={error}>
|
||||||
{/* Label */}
|
|
||||||
<label className="block text-sm font-medium text-gray-700">{label}</label>
|
|
||||||
{/* */}
|
|
||||||
<div className="relative flex rounded-md shadow-sm">
|
<div className="relative flex rounded-md shadow-sm">
|
||||||
{prefix && (
|
{prefix && (
|
||||||
<span className="inline-flex items-center rounded-l-md border-gray-300 bg-orange-200 px-3 font-mono text-sm">
|
<span className="inline-flex items-center rounded-l-md border-gray-300 bg-orange-200 px-3 font-mono text-sm">
|
||||||
@@ -70,10 +68,6 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{description && (
|
</InfoWrapper>
|
||||||
<p className="mt-2 text-sm text-gray-500">{description}</p>
|
|
||||||
)}
|
|
||||||
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { forwardRef, SelectHTMLAttributes } from "react";
|
import { forwardRef, SelectHTMLAttributes } from "react";
|
||||||
|
|
||||||
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
import { InfoWrapper, InfoWrapperProps } from "./InfoWrapper.js";
|
||||||
label: string;
|
|
||||||
description?: string;
|
export interface SelectProps
|
||||||
|
extends SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
|
Omit<InfoWrapperProps, "children"> {
|
||||||
options?: string[];
|
options?: string[];
|
||||||
prefix?: string;
|
|
||||||
suffix?: string;
|
|
||||||
action?: {
|
action?: {
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
action: () => void;
|
action: () => void;
|
||||||
};
|
};
|
||||||
error?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
|
||||||
@@ -19,30 +18,22 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
|
|||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
options,
|
options,
|
||||||
prefix,
|
|
||||||
suffix,
|
|
||||||
action,
|
action,
|
||||||
error,
|
|
||||||
disabled,
|
disabled,
|
||||||
|
error,
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: SelectProps,
|
}: SelectProps,
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<InfoWrapper label={label} description={description} error={error}>
|
||||||
<label
|
|
||||||
htmlFor="location"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
<div className="flex rounded-md shadow-sm">
|
<div className="flex rounded-md shadow-sm">
|
||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
className={`flex h-10 w-full rounded-md border-transparent bg-orange-100 px-3 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 ${
|
||||||
prefix ? "rounded-l-none" : ""
|
action ? "rounded-r-none" : ""
|
||||||
} ${action ? "rounded-r-none" : ""} ${
|
} ${
|
||||||
disabled ? "cursor-not-allowed bg-orange-50 text-orange-200" : ""
|
disabled ? "cursor-not-allowed bg-orange-50 text-orange-200" : ""
|
||||||
}`}
|
}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -64,9 +55,6 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Input(
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{description && (
|
</InfoWrapper>
|
||||||
<p className="mt-2 text-sm text-gray-500">{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,23 +3,24 @@ import type React from "react";
|
|||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
export interface ToggleProps {
|
export interface ToggleProps {
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onChange?: (checked: boolean) => void;
|
onChange?: (checked: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Toggle = ({
|
export const Toggle = ({
|
||||||
|
checked,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
checked,
|
|
||||||
disabled,
|
disabled,
|
||||||
onChange,
|
onChange
|
||||||
}: ToggleProps): JSX.Element => {
|
}: ToggleProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Switch.Group as="div" className="flex items-center justify-between">
|
<Switch.Group as="div" className="flex items-center justify-between">
|
||||||
<span className="flex flex-grow flex-col">
|
<span className="flex flex-grow flex-col">
|
||||||
|
{label && (
|
||||||
<Switch.Label
|
<Switch.Label
|
||||||
as="span"
|
as="span"
|
||||||
className="text-sm font-medium text-gray-900"
|
className="text-sm font-medium text-gray-900"
|
||||||
@@ -27,9 +28,12 @@ export const Toggle = ({
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
<Switch.Description as="span" className="text-sm text-gray-500">
|
<Switch.Description as="span" className="text-sm text-gray-500">
|
||||||
{description}
|
{description}
|
||||||
</Switch.Description>
|
</Switch.Description>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export interface TabbedContentProps {
|
|||||||
|
|
||||||
export const TabbedContent = ({
|
export const TabbedContent = ({
|
||||||
tabs,
|
tabs,
|
||||||
actions,
|
actions
|
||||||
}: TabbedContentProps): JSX.Element => {
|
}: TabbedContentProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Tab.Group as="div" className="flex flex-grow flex-col gap-2 p-4">
|
<Tab.Group as="div" className="flex flex-grow flex-col gap-2 p-4">
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
|
import { produce } from "immer";
|
||||||
import create from "zustand";
|
import create from "zustand";
|
||||||
|
|
||||||
|
export interface RasterSource {
|
||||||
|
enabled: boolean;
|
||||||
|
title: string;
|
||||||
|
tiles: string[];
|
||||||
|
tileSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
selectedDevice: number;
|
selectedDevice: number;
|
||||||
devices: {
|
devices: {
|
||||||
id: number;
|
id: number;
|
||||||
num: number;
|
num: number;
|
||||||
}[];
|
}[];
|
||||||
|
rasterSources: RasterSource[];
|
||||||
|
|
||||||
|
setRasterSources: (sources: RasterSource[]) => void;
|
||||||
|
addRasterSource: (source: RasterSource) => void;
|
||||||
|
removeRasterSource: (index: number) => void;
|
||||||
|
|
||||||
setSelectedDevice: (deviceId: number) => void;
|
setSelectedDevice: (deviceId: number) => void;
|
||||||
addDevice: (device: { id: number; num: number }) => void;
|
addDevice: (device: { id: number; num: number }) => void;
|
||||||
@@ -16,17 +29,40 @@ export const useAppStore = create<AppState>()((set) => ({
|
|||||||
selectedDevice: 0,
|
selectedDevice: 0,
|
||||||
devices: [],
|
devices: [],
|
||||||
currentPage: "messages",
|
currentPage: "messages",
|
||||||
|
rasterSources: [],
|
||||||
|
|
||||||
|
setRasterSources: (sources: RasterSource[]) => {
|
||||||
|
set(
|
||||||
|
produce<AppState>((draft) => {
|
||||||
|
draft.rasterSources = sources;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
addRasterSource: (source: RasterSource) => {
|
||||||
|
set(
|
||||||
|
produce<AppState>((draft) => {
|
||||||
|
draft.rasterSources.push(source);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
removeRasterSource: (index: number) => {
|
||||||
|
set(
|
||||||
|
produce<AppState>((draft) => {
|
||||||
|
draft.rasterSources.splice(index, 1);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
setSelectedDevice: (deviceId) =>
|
setSelectedDevice: (deviceId) =>
|
||||||
set(() => ({
|
set(() => ({
|
||||||
selectedDevice: deviceId,
|
selectedDevice: deviceId
|
||||||
})),
|
})),
|
||||||
addDevice: (device) =>
|
addDevice: (device) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
devices: [...state.devices, device],
|
devices: [...state.devices, device]
|
||||||
})),
|
})),
|
||||||
removeDevice: (deviceId) =>
|
removeDevice: (deviceId) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
devices: state.devices.filter((device) => device.id !== deviceId),
|
devices: state.devices.filter((device) => device.id !== deviceId)
|
||||||
})),
|
}))
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
import type { Device } from "@core/stores/deviceStore.js";
|
import type { Device } from "@core/stores/deviceStore.js";
|
||||||
import { Protobuf, Types } from "@meshtastic/meshtasticjs";
|
import { Types } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
export const subscribeAll = (
|
export const subscribeAll = (
|
||||||
device: Device,
|
device: Device,
|
||||||
connection: Types.ConnectionType
|
connection: Types.ConnectionType
|
||||||
) => {
|
) => {
|
||||||
let myNodeNum = 0;
|
let myNodeNum = 0;
|
||||||
connection.setLogLevel(Protobuf.LogRecord_Level.TRACE);
|
|
||||||
|
|
||||||
// onLogEvent
|
// onLogEvent
|
||||||
// onMeshHeartbeat
|
// onMeshHeartbeat
|
||||||
@@ -43,26 +42,31 @@ export const subscribeAll = (
|
|||||||
device.addWaypointMessage({
|
device.addWaypointMessage({
|
||||||
waypointID: data.id,
|
waypointID: data.id,
|
||||||
ack: rest.packet.from !== myNodeNum,
|
ack: rest.packet.from !== myNodeNum,
|
||||||
...rest,
|
...rest
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onMyNodeInfo.subscribe((nodeInfo) => {
|
connection.onMyNodeInfo.subscribe((nodeInfo) => {
|
||||||
|
console.log("^^^^^^^ GOT MY NODE INFO");
|
||||||
|
|
||||||
device.setHardware(nodeInfo);
|
device.setHardware(nodeInfo);
|
||||||
myNodeNum = nodeInfo.myNodeNum;
|
myNodeNum = nodeInfo.myNodeNum;
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onUserPacket.subscribe((user) => {
|
connection.onUserPacket.subscribe((user) => {
|
||||||
|
console.log("^^^^^^^ GOT USER");
|
||||||
device.addUser(user);
|
device.addUser(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onPositionPacket.subscribe((position) => {
|
connection.onPositionPacket.subscribe((position) => {
|
||||||
|
console.log("^^^^^^^ GOT POSITION");
|
||||||
device.addPosition(position);
|
device.addPosition(position);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onNodeInfoPacket.subscribe((nodeInfo) => {
|
connection.onNodeInfoPacket.subscribe((nodeInfo) => {
|
||||||
|
console.log("^^^^^^^ GOT NODE INFO");
|
||||||
toast(`New Node Discovered: ${nodeInfo.data.user?.shortName ?? "UNK"}`, {
|
toast(`New Node Discovered: ${nodeInfo.data.user?.shortName ?? "UNK"}`, {
|
||||||
icon: "🔎",
|
icon: "🔎"
|
||||||
});
|
});
|
||||||
device.addNodeInfo(nodeInfo);
|
device.addNodeInfo(nodeInfo);
|
||||||
});
|
});
|
||||||
@@ -71,7 +75,7 @@ export const subscribeAll = (
|
|||||||
device.addChannel({
|
device.addChannel({
|
||||||
config: channel.data,
|
config: channel.data,
|
||||||
lastInterraction: new Date(),
|
lastInterraction: new Date(),
|
||||||
messages: [],
|
messages: []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
connection.onConfigPacket.subscribe((config) => {
|
connection.onConfigPacket.subscribe((config) => {
|
||||||
@@ -84,7 +88,7 @@ export const subscribeAll = (
|
|||||||
connection.onMessagePacket.subscribe((messagePacket) => {
|
connection.onMessagePacket.subscribe((messagePacket) => {
|
||||||
device.addMessage({
|
device.addMessage({
|
||||||
...messagePacket,
|
...messagePacket,
|
||||||
ack: messagePacket.packet.from !== myNodeNum,
|
ack: messagePacket.packet.from !== myNodeNum
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
export interface enumLike {
|
||||||
|
[key: number]: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
export const bitwiseEncode = (enumValues: number[]): number => {
|
export const bitwiseEncode = (enumValues: number[]): number => {
|
||||||
return enumValues.reduce((acc, curr) => acc | curr, 0);
|
return enumValues.reduce((acc, curr) => acc | curr, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bitwiseDecode = (value: number, decodeEnum: object): number[] => {
|
export const bitwiseDecode = (
|
||||||
|
value: number,
|
||||||
|
decodeEnum: enumLike
|
||||||
|
): number[] => {
|
||||||
const enumValues = Object.keys(decodeEnum).map(Number).filter(Boolean);
|
const enumValues = Object.keys(decodeEnum).map(Number).filter(Boolean);
|
||||||
|
|
||||||
return enumValues.map((b) => value & b).filter(Boolean);
|
return enumValues.map((b) => value & b).filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { createRoot } from "react-dom/client";
|
|||||||
|
|
||||||
import { App } from "@app/App.js";
|
import { App } from "@app/App.js";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
||||||
const container = document.getElementById("root") as HTMLElement;
|
const container = document.getElementById("root") as HTMLElement;
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
|||||||
+17
-3
@@ -4,7 +4,10 @@ import { Channel } from "@app/components/PageComponents/Channel.js";
|
|||||||
import { Button } from "@components/Button.js";
|
import { Button } from "@components/Button.js";
|
||||||
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
import { QrCodeIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
ArrowDownOnSquareStackIcon,
|
||||||
|
QrCodeIcon
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
export const ChannelsPage = (): JSX.Element => {
|
export const ChannelsPage = (): JSX.Element => {
|
||||||
@@ -17,7 +20,7 @@ export const ChannelsPage = (): JSX.Element => {
|
|||||||
: channel.config.role === Protobuf.Channel_Role.PRIMARY
|
: channel.config.role === Protobuf.Channel_Role.PRIMARY
|
||||||
? "Primary"
|
? "Primary"
|
||||||
: `Channel: ${channel.config.index}`,
|
: `Channel: ${channel.config.index}`,
|
||||||
element: () => <Channel channel={channel.config} />,
|
element: () => <Channel channel={channel.config} />
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,6 +28,17 @@ export const ChannelsPage = (): JSX.Element => {
|
|||||||
<TabbedContent
|
<TabbedContent
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
actions={[
|
actions={[
|
||||||
|
() => (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
iconBefore={<ArrowDownOnSquareStackIcon className="w-4" />}
|
||||||
|
onClick={() => {
|
||||||
|
setQRDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
() => (
|
() => (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -35,7 +49,7 @@ export const ChannelsPage = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
QR Code
|
QR Code
|
||||||
</Button>
|
</Button>
|
||||||
),
|
)
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
import { Map } from "@components/PageComponents/AppConfig/Map.js";
|
||||||
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
|
|
||||||
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
|
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
export const AppConfig = (): JSX.Element => {
|
export const AppConfig = (): JSX.Element => {
|
||||||
const configSections = [
|
const configSections = [
|
||||||
{
|
{
|
||||||
label: "Interface",
|
label: "Map",
|
||||||
element: MQTT,
|
element: Map
|
||||||
},
|
}
|
||||||
{
|
|
||||||
label: "Logging",
|
|
||||||
element: Serial,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Language",
|
|
||||||
element: ExternalNotification,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,37 +18,37 @@ export const DeviceConfig = (): JSX.Element => {
|
|||||||
const configSections = [
|
const configSections = [
|
||||||
{
|
{
|
||||||
label: "User",
|
label: "User",
|
||||||
element: User,
|
element: User
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Device",
|
label: "Device",
|
||||||
element: Device,
|
element: Device
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Position",
|
label: "Position",
|
||||||
element: Position,
|
element: Position
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Power",
|
label: "Power",
|
||||||
element: Power,
|
element: Power
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Network",
|
label: "Network",
|
||||||
element: Network,
|
element: Network,
|
||||||
disabled: !hardware.hasWifi,
|
disabled: !hardware.hasWifi
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Display",
|
label: "Display",
|
||||||
element: Display,
|
element: Display
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "LoRa",
|
label: "LoRa",
|
||||||
element: LoRa,
|
element: LoRa
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Bluetooth",
|
label: "Bluetooth",
|
||||||
element: Bluetooth,
|
element: Bluetooth
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage";
|
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage";
|
||||||
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
|
||||||
@@ -11,37 +11,35 @@ import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js"
|
|||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
export const ModuleConfig = (): JSX.Element => {
|
export const ModuleConfig = (): JSX.Element => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
||||||
|
|
||||||
const configSections = [
|
const configSections = [
|
||||||
{
|
{
|
||||||
label: "MQTT",
|
label: "MQTT",
|
||||||
element: MQTT,
|
element: MQTT
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Serial",
|
label: "Serial",
|
||||||
element: Serial,
|
element: Serial
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "External Notification",
|
label: "External Notification",
|
||||||
element: ExternalNotification,
|
element: ExternalNotification
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Store & Forward",
|
label: "Store & Forward",
|
||||||
element: StoreForward,
|
element: StoreForward
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Range Test",
|
label: "Range Test",
|
||||||
element: RangeTest,
|
element: RangeTest
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Telemetry",
|
label: "Telemetry",
|
||||||
element: Telemetry,
|
element: Telemetry
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Canned Message",
|
label: "Canned Message",
|
||||||
element: CannedMessage,
|
element: CannedMessage
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TabbedContent, TabType } from "@components/layout/page/TabbedContent";
|
|||||||
import {
|
import {
|
||||||
Cog8ToothIcon,
|
Cog8ToothIcon,
|
||||||
CubeTransparentIcon,
|
CubeTransparentIcon,
|
||||||
WindowIcon,
|
WindowIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { AppConfig } from "@pages/Config/AppConfig.js";
|
import { AppConfig } from "@pages/Config/AppConfig.js";
|
||||||
import { DeviceConfig } from "@pages/Config/DeviceConfig.js";
|
import { DeviceConfig } from "@pages/Config/DeviceConfig.js";
|
||||||
@@ -15,18 +15,18 @@ export const ConfigPage = (): JSX.Element => {
|
|||||||
{
|
{
|
||||||
name: "Device Config",
|
name: "Device Config",
|
||||||
icon: <Cog8ToothIcon className="h-4" />,
|
icon: <Cog8ToothIcon className="h-4" />,
|
||||||
element: DeviceConfig,
|
element: DeviceConfig
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Module Config",
|
name: "Module Config",
|
||||||
icon: <CubeTransparentIcon className="h-4" />,
|
icon: <CubeTransparentIcon className="h-4" />,
|
||||||
element: ModuleConfig,
|
element: ModuleConfig
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "App Config",
|
name: "App Config",
|
||||||
icon: <WindowIcon className="h-4" />,
|
icon: <WindowIcon className="h-4" />,
|
||||||
element: AppConfig,
|
element: AppConfig
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return <TabbedContent tabs={tabs} />;
|
return <TabbedContent tabs={tabs} />;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useDevice } from "@core/providers/useDevice.js";
|
|||||||
import {
|
import {
|
||||||
CloudIcon,
|
CloudIcon,
|
||||||
DocumentIcon,
|
DocumentIcon,
|
||||||
SignalIcon,
|
SignalIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { Environment } from "@pages/Extensions/Environment.js";
|
import { Environment } from "@pages/Extensions/Environment.js";
|
||||||
import { FileBrowser } from "@pages/Extensions/FileBrowser";
|
import { FileBrowser } from "@pages/Extensions/FileBrowser";
|
||||||
@@ -18,19 +18,19 @@ export const ExtensionsPage = (): JSX.Element => {
|
|||||||
name: "File Browser",
|
name: "File Browser",
|
||||||
icon: <DocumentIcon className="h-4" />,
|
icon: <DocumentIcon className="h-4" />,
|
||||||
element: FileBrowser,
|
element: FileBrowser,
|
||||||
disabled: !hardware.hasWifi,
|
disabled: !hardware.hasWifi
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Range Test",
|
name: "Range Test",
|
||||||
icon: <SignalIcon className="h-4" />,
|
icon: <SignalIcon className="h-4" />,
|
||||||
element: FileBrowser,
|
element: FileBrowser,
|
||||||
disabled: !hardware.hasWifi,
|
disabled: !hardware.hasWifi
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Environment",
|
name: "Environment",
|
||||||
icon: <CloudIcon className="h-4" />,
|
icon: <CloudIcon className="h-4" />,
|
||||||
element: Environment,
|
element: Environment
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return <TabbedContent tabs={tabs} />;
|
return <TabbedContent tabs={tabs} />;
|
||||||
|
|||||||
+9
-9
@@ -5,7 +5,7 @@ import { JSONTree } from "react-json-tree";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
TabbedContent,
|
TabbedContent,
|
||||||
TabType,
|
TabType
|
||||||
} from "@app/components/layout/page/TabbedContent.js";
|
} from "@app/components/layout/page/TabbedContent.js";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
import { EyeIcon } from "@heroicons/react/24/outline";
|
import { EyeIcon } from "@heroicons/react/24/outline";
|
||||||
@@ -24,32 +24,32 @@ export const InfoPage = (): JSX.Element => {
|
|||||||
{
|
{
|
||||||
name: "Config",
|
name: "Config",
|
||||||
icon: <EyeIcon className="h-4" />,
|
icon: <EyeIcon className="h-4" />,
|
||||||
element: () => <JSONTree theme="monokai" data={config} />,
|
element: () => <JSONTree theme="monokai" data={config} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Module Config",
|
name: "Module Config",
|
||||||
icon: <EyeIcon className="h-4" />,
|
icon: <EyeIcon className="h-4" />,
|
||||||
element: () => <JSONTree theme="monokai" data={moduleConfig} />,
|
element: () => <JSONTree theme="monokai" data={moduleConfig} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Hardware",
|
name: "Hardware",
|
||||||
icon: <EyeIcon className="h-4" />,
|
icon: <EyeIcon className="h-4" />,
|
||||||
element: () => <JSONTree theme="monokai" data={hardware} />,
|
element: () => <JSONTree theme="monokai" data={hardware} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nodes",
|
name: "Nodes",
|
||||||
icon: <EyeIcon className="h-4" />,
|
icon: <EyeIcon className="h-4" />,
|
||||||
element: () => <JSONTree theme="monokai" data={nodes} />,
|
element: () => <JSONTree theme="monokai" data={nodes} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Waypoints",
|
name: "Waypoints",
|
||||||
icon: <EyeIcon className="h-4" />,
|
icon: <EyeIcon className="h-4" />,
|
||||||
element: () => <JSONTree theme="monokai" data={waypoints} />,
|
element: () => <JSONTree theme="monokai" data={waypoints} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Connection",
|
name: "Connection",
|
||||||
icon: <EyeIcon className="h-4" />,
|
icon: <EyeIcon className="h-4" />,
|
||||||
element: () => <JSONTree theme="monokai" data={connection} />,
|
element: () => <JSONTree theme="monokai" data={connection} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Serial Logs",
|
name: "Serial Logs",
|
||||||
@@ -62,8 +62,8 @@ export const InfoPage = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
),
|
)
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return <TabbedContent tabs={tabs} />;
|
return <TabbedContent tabs={tabs} />;
|
||||||
|
|||||||
+16
-5
@@ -1,25 +1,27 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
import { Map, Marker, useMap } from "react-map-gl";
|
import { Layer, Map, Marker, Source, useMap } from "react-map-gl";
|
||||||
import { base16 } from "rfc4648";
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
import { Card } from "@app/components/Card.js";
|
import { Card } from "@app/components/Card.js";
|
||||||
import { IconButton } from "@app/components/IconButton.js";
|
import { IconButton } from "@app/components/IconButton.js";
|
||||||
import { Mono } from "@app/components/Mono.js";
|
import { Mono } from "@app/components/Mono.js";
|
||||||
|
import { useAppStore } from "@app/core/stores/appStore.js";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import {
|
import {
|
||||||
EllipsisHorizontalCircleIcon,
|
EllipsisHorizontalCircleIcon,
|
||||||
MapPinIcon,
|
MapPinIcon
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export const MapPage = (): JSX.Element => {
|
export const MapPage = (): JSX.Element => {
|
||||||
const { nodes, waypoints } = useDevice();
|
const { nodes, waypoints } = useDevice();
|
||||||
|
const { rasterSources } = useAppStore();
|
||||||
const { current: map } = useMap();
|
const { current: map } = useMap();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-grow">
|
<div className="h-full flex-grow">
|
||||||
<div className="absolute right-0 top-0 z-10 m-2">
|
<div className="absolute right-0 top-0 z-10 m-2">
|
||||||
<Card className="flex-col p-3">
|
<Card className="flex-col p-3">
|
||||||
<div className="p-1 text-lg font-medium">Title</div>
|
<div className="p-1 text-lg font-medium">Title</div>
|
||||||
@@ -51,9 +53,9 @@ export const MapPage = (): JSX.Element => {
|
|||||||
map?.flyTo({
|
map?.flyTo({
|
||||||
center: [
|
center: [
|
||||||
n.data.position.longitudeI / 1e7,
|
n.data.position.longitudeI / 1e7,
|
||||||
n.data.position.latitudeI / 1e7,
|
n.data.position.latitudeI / 1e7
|
||||||
],
|
],
|
||||||
zoom: 10,
|
zoom: 10
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -62,6 +64,10 @@ export const MapPage = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{/* */}
|
||||||
|
{rasterSources.map((source, index) => (
|
||||||
|
<div key={index}>{source.title}Tst</div>
|
||||||
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Map
|
<Map
|
||||||
@@ -81,6 +87,11 @@ export const MapPage = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
))}
|
))}
|
||||||
|
{rasterSources.map((source, index) => (
|
||||||
|
<Source key={index} type="raster" {...source}>
|
||||||
|
<Layer type="raster" />
|
||||||
|
</Source>
|
||||||
|
))}
|
||||||
{nodes.map((n) => {
|
{nodes.map((n) => {
|
||||||
if (n.data.position?.latitudeI) {
|
if (n.data.position?.latitudeI) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type React from "react";
|
|||||||
import { IconButton } from "@app/components/IconButton.js";
|
import { IconButton } from "@app/components/IconButton.js";
|
||||||
import {
|
import {
|
||||||
TabbedContent,
|
TabbedContent,
|
||||||
TabType,
|
TabType
|
||||||
} from "@components/layout/page/TabbedContent.js";
|
} from "@components/layout/page/TabbedContent.js";
|
||||||
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.js";
|
import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.js";
|
||||||
import { useDevice } from "@core/providers/useDevice.js";
|
import { useDevice } from "@core/providers/useDevice.js";
|
||||||
@@ -21,7 +21,7 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
? "Primary"
|
? "Primary"
|
||||||
: `Ch ${channel.config.index}`,
|
: `Ch ${channel.config.index}`,
|
||||||
element: () => <ChannelChat channel={channel} />,
|
element: () => <ChannelChat channel={channel} />,
|
||||||
disabled: channel.config.role === Protobuf.Channel_Role.DISABLED,
|
disabled: channel.config.role === Protobuf.Channel_Role.DISABLED
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
setActivePage("channels");
|
setActivePage("channels");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -108,7 +108,7 @@ export const PeersPage = (): JSX.Element => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (connection) {
|
if (connection) {
|
||||||
void toast.promise(
|
void toast.promise(
|
||||||
connection.getMetadata(node.data.num),
|
connection.getMetadata({ nodeNum: node.data.num }),
|
||||||
{
|
{
|
||||||
loading: "Requesting Metadata...",
|
loading: "Requesting Metadata...",
|
||||||
success: "Recieved Metadata",
|
success: "Recieved Metadata",
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { IsArray, IsBoolean, IsNumber, IsString } from "class-validator";
|
||||||
|
|
||||||
|
import type { RasterSource } from "@app/core/stores/appStore.js";
|
||||||
|
|
||||||
|
export class MapValidation {
|
||||||
|
@IsArray()
|
||||||
|
rasterSources: MapValidation_RasterSources[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapValidation_RasterSources implements RasterSource {
|
||||||
|
@IsBoolean()
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
// @IsUrl()
|
||||||
|
tiles: string[];
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
tileSize: number;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsBoolean, IsEnum } from "class-validator";
|
import { IsBoolean, IsEnum, IsInt } from "class-validator";
|
||||||
|
|
||||||
import { Protobuf } from "@meshtastic/meshtasticjs";
|
import { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
@@ -11,4 +11,10 @@ export class DeviceValidation implements Protobuf.Config_DeviceConfig {
|
|||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
debugLogEnabled: boolean;
|
debugLogEnabled: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
buttonGpio: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
buzzerGpio: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,7 @@ export class DisplayValidation implements Protobuf.Config_DisplayConfig {
|
|||||||
|
|
||||||
@IsEnum(Protobuf.Config_DisplayConfig_DisplayUnits)
|
@IsEnum(Protobuf.Config_DisplayConfig_DisplayUnits)
|
||||||
units: Protobuf.Config_DisplayConfig_DisplayUnits;
|
units: Protobuf.Config_DisplayConfig_DisplayUnits;
|
||||||
|
|
||||||
|
@IsEnum(Protobuf.Config_DisplayConfig_OledType)
|
||||||
|
oled: Protobuf.Config_DisplayConfig_OledType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,11 @@ export class NetworkValidation implements Protobuf.Config_NetworkConfig {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
wifiEnabled: boolean;
|
wifiEnabled: boolean;
|
||||||
|
|
||||||
@IsEnum(Protobuf.Config_NetworkConfig_WiFiMode)
|
@Length(1, 33)
|
||||||
wifiMode: Protobuf.Config_NetworkConfig_WiFiMode;
|
|
||||||
|
|
||||||
@Length(0, 33) //min 1
|
|
||||||
@IsOptional({})
|
@IsOptional({})
|
||||||
wifiSsid: string;
|
wifiSsid: string;
|
||||||
|
|
||||||
@Length(0, 64) //min 8
|
@Length(8, 64)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
wifiPsk: string;
|
wifiPsk: string;
|
||||||
|
|
||||||
@@ -26,7 +23,7 @@ export class NetworkValidation implements Protobuf.Config_NetworkConfig {
|
|||||||
@IsEnum(Protobuf.Config_NetworkConfig_EthMode)
|
@IsEnum(Protobuf.Config_NetworkConfig_EthMode)
|
||||||
ethMode: Protobuf.Config_NetworkConfig_EthMode;
|
ethMode: Protobuf.Config_NetworkConfig_EthMode;
|
||||||
|
|
||||||
ethConfig: NetworkValidation_IpV4Config;
|
ipv4Config: NetworkValidation_IpV4Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NetworkValidation_IpV4Config
|
export class NetworkValidation_IpV4Config
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export class PositionValidation implements Protobuf.Config_PositionConfig {
|
|||||||
@IsInt()
|
@IsInt()
|
||||||
positionFlags: number;
|
positionFlags: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
rxGpio: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
txGpio: number;
|
||||||
|
|
||||||
// fixed position fields
|
// fixed position fields
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
fixedAlt: number;
|
fixedAlt: number;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsInt } from "class-validator";
|
import { IsBoolean, IsInt } from "class-validator";
|
||||||
|
|
||||||
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
import type { Protobuf } from "@meshtastic/meshtasticjs";
|
||||||
|
|
||||||
@@ -13,9 +13,15 @@ export class ExternalNotificationValidation
|
|||||||
@IsInt()
|
@IsInt()
|
||||||
output: number;
|
output: number;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
alertMessage: boolean;
|
alertMessage: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
alertBell: boolean;
|
alertBell: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
usePwm: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -12,10 +12,10 @@ module.exports = {
|
|||||||
"Consolas",
|
"Consolas",
|
||||||
"Liberation Mono",
|
"Liberation Mono",
|
||||||
"Courier New",
|
"Courier New",
|
||||||
"monospace",
|
"monospace"
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
extend: {},
|
extend: {}
|
||||||
},
|
},
|
||||||
plugins: [require("@tailwindcss/forms")],
|
plugins: [require("@tailwindcss/forms")]
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-7
@@ -18,15 +18,15 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
EnvironmentPlugin({
|
EnvironmentPlugin({
|
||||||
COMMIT_HASH: hash,
|
COMMIT_HASH: hash
|
||||||
}),
|
})
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
target: "esnext",
|
target: "esnext",
|
||||||
assetsDir: "",
|
assetsDir: "",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
plugins: [visualizer()],
|
plugins: [visualizer()]
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@@ -34,7 +34,7 @@ export default defineConfig({
|
|||||||
"@pages": resolve(__dirname, "./src/pages"),
|
"@pages": resolve(__dirname, "./src/pages"),
|
||||||
"@components": resolve(__dirname, "./src/components"),
|
"@components": resolve(__dirname, "./src/components"),
|
||||||
"@core": resolve(__dirname, "./src/core"),
|
"@core": resolve(__dirname, "./src/core"),
|
||||||
"@layouts": resolve(__dirname, "./src/layouts"),
|
"@layouts": resolve(__dirname, "./src/layouts")
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user