Browse Source

Updated channel editor + fixes

pull/2/head
Sacha Weatherstone 5 years ago
parent
commit
1f8170130d
  1. 14
      package.json
  2. 537
      pnpm-lock.yaml
  3. 33
      src/App.tsx
  4. 70
      src/components/Channel.tsx
  5. 116
      src/components/LoraConfig.tsx
  6. 9
      src/components/generic/form/Input.tsx
  7. 2
      src/components/generic/form/Select.tsx
  8. 18
      src/components/menu/Navigation.tsx
  9. 3
      src/core/connection.ts
  10. 1
      src/core/router.ts
  11. 21
      src/core/slices/meshtasticSlice.ts
  12. 14
      src/pages/About.tsx
  13. 117
      src/pages/Plugins/ExternalNotification.tsx
  14. 21
      src/pages/Plugins/Index.tsx
  15. 4
      src/pages/Plugins/RangeTest.tsx
  16. 118
      src/pages/Plugins/Serial.tsx
  17. 14
      src/pages/settings/Channels.tsx
  18. 72
      src/pages/settings/Connection.tsx
  19. 3
      src/pages/settings/Position.tsx
  20. 2
      src/pages/settings/User.tsx
  21. 10
      todo.txt

14
package.json

@ -13,10 +13,10 @@
},
"dependencies": {
"@headlessui/react": "^1.4.2",
"@meshtastic/meshtasticjs": "^0.6.27",
"@meshtastic/meshtasticjs": "^0.6.28",
"@reduxjs/toolkit": "^1.6.2",
"boring-avatars": "^1.5.8",
"i18next": "^21.4.2",
"i18next": "^21.5.1",
"i18next-browser-languagedetector": "^6.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@ -33,13 +33,13 @@
"use-breakpoint": "^2.0.2"
},
"devDependencies": {
"@types/react": "^17.0.34",
"@types/react": "^17.0.35",
"@types/react-dom": "^17.0.11",
"@types/react-file-icon": "^1.0.1",
"@types/w3c-web-serial": "^1.0.2",
"@types/web-bluetooth": "^0.0.11",
"@typescript-eslint/eslint-plugin": "^5.3.1",
"@typescript-eslint/parser": "^5.3.1",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@verypossible/eslint-config": "^1.6.1",
"@vitejs/plugin-react": "^1.0.9",
"autoprefixer": "^10.4.0",
@ -58,7 +58,7 @@
"tar": "^6.1.11",
"typescript": "^4.4.4",
"vite": "^2.6.14",
"vite-plugin-pwa": "^0.11.3",
"workbox-window": "^6.3.0"
"vite-plugin-pwa": "^0.11.5",
"workbox-window": "^6.4.1"
}
}

537
pnpm-lock.yaml

File diff suppressed because it is too large

33
src/App.tsx

@ -22,12 +22,13 @@ import {
setReady,
} from '@core/slices/meshtasticSlice';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
SettingsManager,
Types,
} from '@meshtastic/meshtasticjs';
import { About } from '@pages/About';
import { Messages } from '@pages/Messages';
import { Nodes } from '@pages/Nodes/Index';
import { Settings } from '@pages/settings/Index';
@ -55,15 +56,28 @@ const App = (): JSX.Element => {
'http://meshtastic.local';
React.useEffect(() => {
const connectionMethod = localStorage.getItem('connectionMethod');
switch (connectionMethod) {
case 'serial':
setConnection(new ISerialConnection());
//show connection dialogue
break;
case 'bluetooth':
setConnection(new IBLEConnection());
//show connection dialogue
break;
default:
setConnection(new IHTTPConnection());
void connection.connect({
address: connectionURL,
tls: false,
receiveBatchRequests: false,
fetchInterval: 2000,
});
break;
}
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
setConnection(new IHTTPConnection());
void connection.connect({
address: connectionURL,
tls: false,
receiveBatchRequests: false,
fetchInterval: 2000,
});
}, [hostOverrideEnabled, hostOverride, connectionURL]);
React.useEffect(() => {
@ -168,7 +182,6 @@ const App = (): JSX.Element => {
{route.name === 'nodes' && <Nodes />}
{route.name === 'plugins' && <Plugins />}
{route.name === 'settings' && <Settings />}
{route.name === 'about' && <About />}
{route.name === false && <NotFound />}
</div>
</div>

70
src/components/Channel.tsx

@ -13,13 +13,17 @@ import { IconButton } from './generic/IconButton';
export interface ChannelProps {
channel: Protobuf.Channel;
hideEnabled?: boolean;
}
export const Channel = ({ channel }: ChannelProps): JSX.Element => {
export const Channel = ({
channel,
hideEnabled,
}: ChannelProps): JSX.Element => {
const [edit, setEdit] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit, formState } = useForm<{
const { register, handleSubmit } = useForm<{
enabled: boolean;
settings: {
name: string;
@ -33,11 +37,12 @@ export const Channel = ({ channel }: ChannelProps): JSX.Element => {
};
}>({
defaultValues: {
enabled:
channel.role ===
(Protobuf.Channel_Role.PRIMARY || Protobuf.Channel_Role.SECONDARY)
? true
: false,
enabled: [
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
].find((role) => role === channel.role)
? true
: false,
settings: {
name: channel.settings?.name,
bandwidth: channel.settings?.bandwidth,
@ -54,9 +59,12 @@ export const Channel = ({ channel }: ChannelProps): JSX.Element => {
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
const adminChannel = Protobuf.Channel.create({
role: data.enabled
? Protobuf.Channel_Role.SECONDARY
: Protobuf.Channel_Role.DISABLED,
role:
channel.role === Protobuf.Channel_Role.PRIMARY
? Protobuf.Channel_Role.PRIMARY
: data.enabled
? Protobuf.Channel_Role.SECONDARY
: Protobuf.Channel_Role.DISABLED,
index: channel.index,
settings: {
...data.settings,
@ -75,41 +83,24 @@ export const Channel = ({ channel }: ChannelProps): JSX.Element => {
{edit ? (
<>
{loading && <Loading />}
<div className="my-auto space-x-2">
<form>
<div className="flex space-x-2">
{/* @todo: change to disable & make primary buttons */}
<div className="flex my-auto">
{/* TODO: get gap working */}
<form className="gap-3">
{/* @todo: change to disable & make primary buttons */}
{!hideEnabled && (
<Checkbox
label="Enabled"
{...register('enabled', { valueAsNumber: true })}
/>
</div>
)}
<Input label="Name" {...register('settings.name')} />
<Input label="Pre-Shared Key" {...register('settings.psk')} />
<Input
label="Bandwidth"
type="number"
{...register('settings.bandwidth', { valueAsNumber: true })}
/>
<Input
label="Spread Factor"
type="number"
min={7}
max={12}
{...register('settings.spreadFactor', { valueAsNumber: true })}
/>
<Input
label="Coding Rate"
type="number"
{...register('settings.codingRate', { valueAsNumber: true })}
/>
<Input
label="Transmit Power"
type="number"
{...register('settings.txPower', { valueAsNumber: true })}
label="Pre-Shared Key"
type="password"
{...register('settings.psk')}
/>
<Checkbox
label="Upling Enabled"
label="Uplink Enabled"
{...register('settings.uplinkEnabled')}
/>
<Checkbox
@ -132,7 +123,10 @@ export const Channel = ({ channel }: ChannelProps): JSX.Element => {
<div className="flex my-auto space-x-2">
<div
className={`h-3 my-auto w-3 rounded-full ${
channel.role === Protobuf.Channel_Role.SECONDARY
[
Protobuf.Channel_Role.SECONDARY,
Protobuf.Channel_Role.PRIMARY,
].find((role) => role === channel.role)
? 'bg-green-500'
: 'bg-gray-400'
}`}

116
src/components/LoraConfig.tsx

@ -0,0 +1,116 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { Loading } from '@components/generic/Loading';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { connection } from '../core/connection';
import { Card } from './generic/Card';
import { Checkbox } from './generic/form/Checkbox';
import { Input } from './generic/form/Input';
export interface LoraConfigProps {
channel: Protobuf.Channel;
}
export const LoraConfig = ({ channel }: LoraConfigProps): JSX.Element => {
const [loading, setLoading] = React.useState(false);
const { register, handleSubmit } = useForm<{
enabled: boolean;
settings: {
name: string;
bandwidth?: number;
codingRate?: number;
spreadFactor?: number;
downlinkEnabled?: boolean;
uplinkEnabled?: boolean;
txPower?: number;
psk?: string;
};
}>({
defaultValues: {
enabled:
channel.role ===
(Protobuf.Channel_Role.PRIMARY || Protobuf.Channel_Role.SECONDARY)
? true
: false,
settings: {
name: channel.settings?.name,
bandwidth: channel.settings?.bandwidth,
codingRate: channel.settings?.codingRate,
spreadFactor: channel.settings?.spreadFactor,
downlinkEnabled: channel.settings?.downlinkEnabled,
uplinkEnabled: channel.settings?.uplinkEnabled,
txPower: channel.settings?.txPower,
psk: new TextDecoder().decode(channel.settings?.psk),
},
},
});
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
const adminChannel = Protobuf.Channel.create({
role: data.enabled
? Protobuf.Channel_Role.SECONDARY
: Protobuf.Channel_Role.DISABLED,
index: channel.index,
settings: {
...data.settings,
psk: new TextEncoder().encode(data.settings.psk),
},
});
await connection.setChannel(adminChannel, (): Promise<void> => {
setLoading(false);
return Promise.resolve();
});
});
return (
<Card>
{loading && <Loading />}
<div className="w-full max-w-3xl p-10 md:max-w-xl">
{/* TODO: get gap working */}
<form onSubmit={onSubmit}>
<Input
label="Bandwidth"
type="number"
suffix="MHz"
{...register('settings.bandwidth', { valueAsNumber: true })}
/>
<Input
label="Spread Factor"
type="number"
suffix="CPS"
min={7}
max={12}
{...register('settings.spreadFactor', {
valueAsNumber: true,
})}
/>
<Input
label="Coding Rate"
type="number"
{...register('settings.codingRate', { valueAsNumber: true })}
/>
<Input
label="Transmit Power"
type="number"
suffix="dBm"
{...register('settings.txPower', { valueAsNumber: true })}
/>
<Checkbox
label="Uplink Enabled"
{...register('settings.uplinkEnabled')}
/>
<Checkbox
label="Downlink Enabled"
{...register('settings.downlinkEnabled')}
/>
</form>
</div>
</Card>
);
};

9
src/components/generic/form/Input.tsx

@ -9,10 +9,12 @@ interface InputProps extends DefaultInputProps {
label?: string;
error?: string;
action?: JSX.Element;
prefix?: string;
suffix?: string;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
function Input({ label, error, action, ...props }: InputProps, ref) {
function Input({ label, error, action, suffix, ...props }: InputProps, ref) {
return (
<div className="w-full">
{label && <Label label={label} error={error} />}
@ -22,6 +24,11 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
className="w-full h-10 px-3 py-2 bg-transparent focus:outline-none"
{...props}
/>
{suffix && (
<span className="my-auto mr-3 text-sm font-medium text-gray-500 dark:text-gray-400">
{suffix}
</span>
)}
{action && <div className="flex mr-1">{action}</div>}
</InputWrapper>
</div>

2
src/components/generic/form/Select.tsx

@ -30,7 +30,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
<select
ref={ref}
className={`w-full rounded-md bg-transparent focus:outline-none focus:border-primary ${
small ? 'p-1' : 'h-10 px-2'
small ? 'm-1' : 'h-10 mx-2'
}`}
disabled={
props.disabled

18
src/components/menu/Navigation.tsx

@ -1,12 +1,6 @@
import type React from 'react';
import {
FiGrid,
FiInfo,
FiMessageSquare,
FiPackage,
FiSettings,
} from 'react-icons/fi';
import { FiGrid, FiMessageSquare, FiPackage, FiSettings } from 'react-icons/fi';
import { Button } from '@components/generic/Button';
import { routes, useRoute } from '@core/router';
@ -66,16 +60,6 @@ export const Navigation = ({
Settings
</Button>
</div>
<div onClick={onClick}>
<Button
icon={<FiInfo className="w-6 h-6" />}
className="w-full md:w-auto"
active={route.name === 'about'}
{...routes.about().link}
>
About
</Button>
</div>
</div>
);
};

3
src/core/connection.ts

@ -8,6 +8,9 @@ type connectionType = IBLEConnection | IHTTPConnection | ISerialConnection;
export let connection: connectionType = new IHTTPConnection();
export const ble = new IBLEConnection();
export const serial = new ISerialConnection();
export const setConnection = (conn: connectionType): void => {
connection = conn;
};

1
src/core/router.ts

@ -5,5 +5,4 @@ export const { RouteProvider, useRoute, routes } = createRouter({
nodes: defineRoute('/nodes'),
plugins: defineRoute('/plugins'),
settings: defineRoute('/settings'),
about: defineRoute('/about'),
});

21
src/core/slices/meshtasticSlice.ts

@ -21,7 +21,6 @@ interface MeshtasticState {
lastMeshInterraction: number;
ready: boolean;
myNodeInfo: Protobuf.MyNodeInfo;
myNode: Protobuf.NodeInfo;
users: Types.UserPacket[];
positionPackets: Types.PositionPacket[];
nodes: Protobuf.NodeInfo[];
@ -38,13 +37,14 @@ const initialState: MeshtasticState = {
lastMeshInterraction: 0,
ready: false,
myNodeInfo: Protobuf.MyNodeInfo.create(),
myNode: Protobuf.NodeInfo.create(),
users: [],
positionPackets: [],
nodes: [],
channels: [],
preferences: Protobuf.RadioConfig_UserPreferences.create(),
messages: [],
//todo implement
// connectionMethod: localStorage.getItem('connectionMethod'),
hostOverrideEnabled:
localStorage.getItem('hostOverrideEnabled') === 'true' ?? false,
hostOverride: localStorage.getItem('hostOverride') ?? '',
@ -83,12 +83,21 @@ export const meshtasticSlice = createSlice({
}
},
addPosition: (state, action: PayloadAction<Types.PositionPacket>) => {
state.positionPackets.push(action.payload);
if (
state.positionPackets.findIndex(
(position) => position.packet.from === action.payload.packet.from,
) !== -1
) {
state.positionPackets = state.positionPackets.map((position) => {
return position.packet.from === action.payload.packet.from
? action.payload
: position;
});
} else {
state.positionPackets.push(action.payload);
}
},
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
if (action.payload.num === state.myNodeInfo.myNodeNum) {
state.myNode = action.payload;
}
if (
state.nodes.findIndex((node) => node.num === action.payload.num) !== -1
) {

14
src/pages/About.tsx

@ -1,14 +0,0 @@
import type React from 'react';
import { Card } from '@components/generic/Card';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
export const About = (): JSX.Element => {
return (
<PrimaryTemplate title="meshtastic-web" tagline="About">
<Card title="Project desc" description="...">
<p className="p-10">Content</p>
</Card>
</PrimaryTemplate>
);
};

117
src/pages/Plugins/ExternalNotification.tsx

@ -0,0 +1,117 @@
import type React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface ExternalNotificationProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const ExternalNotification = ({
navOpen,
setNavOpen,
}: ExternalNotificationProps): JSX.Element => {
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell:
preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs:
preferences.extNotificationPluginOutputMs,
},
});
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});
const watchExternalNotificationPluginEnabled = useWatch({
control,
name: 'extNotificationPluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="External Notification"
tagline="Plugin"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<div className="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Plugin Enabled"
{...register('extNotificationPluginEnabled')}
/>
<Checkbox
label="Active"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginActive')}
/>
<Checkbox
label="Bell"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginAlertBell')}
/>
<Checkbox
label="Message"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginAlertMessage')}
/>
<Input
type="number"
label="Output"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginOutput', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Output MS"
disabled={!watchExternalNotificationPluginEnabled}
{...register('extNotificationPluginOutputMs', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};

21
src/pages/Plugins/Index.tsx

@ -1,11 +1,13 @@
import type React from 'react';
import { FiFileText, FiRss } from 'react-icons/fi';
import { FiAlignLeft, FiBell, FiFileText, FiRss } from 'react-icons/fi';
import { PageLayout } from '@components/templates/PageLayout';
import { ExternalNotification } from './ExternalNotification';
import { Files } from './Files';
import { RangeTest } from './RangeTest';
import { Serial } from './Serial';
export const Plugins = (): JSX.Element => {
return (
@ -22,8 +24,23 @@ export const Plugins = (): JSX.Element => {
description: 'HTTP only file browser',
icon: <FiFileText className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'External Notification',
description: 'External hardware alerts',
icon: <FiBell className="flex-shrink-0 w-6 h-6" />,
},
{
title: 'Serial',
description: 'Send serial data over the mesh',
icon: <FiAlignLeft className="flex-shrink-0 w-6 h-6" />,
},
]}
panels={[
<RangeTest key={1} />,
<Files key={2} />,
<ExternalNotification key={3} />,
<Serial key={4} />,
]}
panels={[<RangeTest key={1} />, <Files key={2} />]}
/>
);
};

4
src/pages/Plugins/RangeTest.tsx

@ -1,7 +1,6 @@
import type React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
@ -23,7 +22,6 @@ export const RangeTest = ({
navOpen,
setNavOpen,
}: RangeTestProps): JSX.Element => {
const { t } = useTranslation();
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const { register, handleSubmit, formState, reset, control } =
@ -66,7 +64,7 @@ export const RangeTest = ({
}
>
<div className="w-full space-y-4">
<Card title="Range Test" description="Settings">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox

118
src/pages/Plugins/Serial.tsx

@ -0,0 +1,118 @@
import type React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiMenu } from 'react-icons/fi';
import { FormFooter } from '@app/components/FormFooter';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { IconButton } from '@components/generic/IconButton';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import type { RadioConfig_UserPreferences } from '@meshtastic/meshtasticjs/dist/generated';
export interface SerialProps {
navOpen?: boolean;
setNavOpen?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const Serial = ({ navOpen, setNavOpen }: SerialProps): JSX.Element => {
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const { register, handleSubmit, formState, reset, control } =
useForm<RadioConfig_UserPreferences>({
defaultValues: {
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTimeout: preferences.serialpluginTimeout,
serialpluginTxd: preferences.serialpluginTxd,
},
});
const onSubmit = handleSubmit((data) => {
void connection.setPreferences(data);
});
const watchSerialPluginEnabled = useWatch({
control,
name: 'serialpluginEnabled',
defaultValue: false,
});
return (
<PrimaryTemplate
title="Serial"
tagline="Plugin"
leftButton={
<IconButton
icon={<FiMenu className="w-5 h-5" />}
onClick={(): void => {
setNavOpen && setNavOpen(!navOpen);
}}
/>
}
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
clearAction={reset}
/>
}
>
<div className="w-full space-y-4">
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form onSubmit={onSubmit}>
<Checkbox
label="Plugin Enabled"
{...register('serialpluginEnabled')}
/>
<Checkbox
label="Echo"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginEcho')}
/>
<Input
type="number"
label="RX"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginRxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="TX"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginTxd', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Mode"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginMode', {
valueAsNumber: true,
})}
/>
<Input
type="number"
label="Timeout"
disabled={!watchSerialPluginEnabled}
{...register('serialpluginTimeout', {
valueAsNumber: true,
})}
/>
</form>
</div>
</Card>
</div>
</PrimaryTemplate>
);
};

14
src/pages/settings/Channels.tsx

@ -1,9 +1,9 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu, FiSave } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { LoraConfig } from '@app/components/LoraConfig';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Channel } from '@components/Channel';
@ -22,10 +22,7 @@ export const Channels = ({
navOpen,
setNavOpen,
}: ChannelsProps): JSX.Element => {
const { t } = useTranslation();
const channels = useAppSelector((state) => state.meshtastic.channels).filter(
(channel) => channel.index !== 0,
);
const channels = useAppSelector((state) => state.meshtastic.channels);
const [debug, setDebug] = React.useState(false);
return (
@ -61,11 +58,16 @@ export const Channels = ({
}
>
<div className="space-y-4">
{channels[0] && <LoraConfig channel={channels[0]} />}
<Card>
<Cover enabled={debug} content={<JSONPretty data={channels} />} />
<div className="w-full p-4 space-y-2 md:p-10">
{channels.map((channel) => (
<Channel key={channel.index} channel={channel} />
<Channel
key={channel.index}
channel={channel}
hideEnabled={channel.index === 0}
/>
))}
<div className="flex justify-between">

72
src/pages/settings/Connection.tsx

@ -1,12 +1,12 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCheck, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { FormFooter } from '@app/components/FormFooter';
import { connection, setConnection } from '@app/core/connection';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { ble, connection, serial, setConnection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/redux';
import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
@ -40,20 +40,18 @@ export const Connection = ({
navOpen,
setNavOpen,
}: ConnectionProps): JSX.Element => {
const dispatch = useAppDispatch();
const [selectedConnType, setSelectedConnType] = React.useState(connType.HTTP);
const [bleDevices, setBleDevices] = React.useState<BluetoothDevice[]>([]);
const [serialDevices, setSerialDevices] = React.useState<SerialPort[]>([]);
const [httpIpSource, setHttpIpSource] = React.useState<'local' | 'remote'>(
'local',
);
const { t } = useTranslation();
const hostOverrideEnabled = useAppSelector(
(state) => state.meshtastic.hostOverrideEnabled,
);
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
const { register, handleSubmit, formState, reset } = useForm<{
const { formState, reset } = useForm<{
method: connType;
}>({
defaultValues: {
@ -83,30 +81,26 @@ export const Connection = ({
await connection.connect(params);
};
const updateBleDeviceList = async (): Promise<void> => {
const updateBleDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await ble.getDevices();
setBleDevices(devices);
};
}, []);
const updateSerialDeviceList = async (): Promise<void> => {
const updateSerialDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await serial.getPorts();
console.log(devices);
setSerialDevices(devices);
};
// React.useEffect(() => {
// if (selectedConnType === connType.BLE) {
// void updateBleDeviceList();
// }
// if (selectedConnType === connType.SERIAL) {
// void updateSerialDeviceList();
// }
// }, [selectedConnType]);
}, []);
const onSubmit = handleSubmit((data) => {
// void connection.setOwner(data);
});
React.useEffect(() => {
if (selectedConnType === connType.BLE) {
void updateBleDeviceList();
}
if (selectedConnType === connType.SERIAL) {
void updateSerialDeviceList();
}
}, [selectedConnType, updateBleDeviceList, updateSerialDeviceList]);
const connectionURL: string = hostOverrideEnabled
? hostOverride
@ -115,9 +109,6 @@ export const Connection = ({
: (import.meta.env.VITE_PUBLIC_DEVICE_IP as string) ??
'http://meshtastic.local';
const ble = new IBLEConnection();
const serial = new ISerialConnection();
return (
<PrimaryTemplate
title="Connection"
@ -133,14 +124,16 @@ export const Connection = ({
footer={
<FormFooter
dirty={formState.isDirty}
saveAction={onSubmit}
saveAction={(): void => {
return;
}}
clearAction={reset}
/>
}
>
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<form className="space-y-2">
<Select
label="Method"
optionsEnum={connType}
@ -184,7 +177,7 @@ export const Connection = ({
</Button>
<Button
border
onClick={async () => {
onClick={async (): Promise<void> => {
await ble.getDevice();
}}
>
@ -193,7 +186,7 @@ export const Connection = ({
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{bleDevices.map((device) => (
{bleDevices.map((device, index) => (
<div
onClick={async (): Promise<void> => {
console.log('clicked');
@ -203,7 +196,7 @@ export const Connection = ({
});
}}
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={device.id}
key={index}
>
<div className="my-auto">{device.name}</div>
<IconButton
@ -224,13 +217,13 @@ export const Connection = ({
{selectedConnType === connType.SERIAL && (
<div>
<div className="flex space-x-2">
<Button border onClick={updateBleDeviceList}>
<Button border onClick={updateSerialDeviceList}>
Refresh List
</Button>
<Button
border
onClick={async () => {
await serial.getPort();
onClick={async (): Promise<void> => {
console.log(await serial.getPort());
}}
>
New Device
@ -238,10 +231,10 @@ export const Connection = ({
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{serialDevices.map((device) => (
{serialDevices.map((device, index) => (
<div
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={device.getInfo().usbProductId}
key={index}
>
<div className="my-auto">
{device.getInfo().usbProductId}
@ -256,12 +249,21 @@ export const Connection = ({
}}
icon={<FiCheck />}
/>
<JSONPretty data={device.getInfo()} />
</div>
))}
</div>
</div>
)}
</form>
<Button
border
onClick={(): void => {
connection.disconnect();
}}
>
Disconnect
</Button>
</div>
</Card>
</PrimaryTemplate>

3
src/pages/settings/Position.tsx

@ -82,8 +82,9 @@ export const Position = ({
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Input
label="Broadcast Interval (seconds)"
label="Broadcast Interval"
type="number"
suffix="Seconds"
{...register('positionBroadcastSecs', { valueAsNumber: true })}
/>
<Select

2
src/pages/settings/User.tsx

@ -1,7 +1,6 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { base16 } from 'rfc4648';
@ -25,7 +24,6 @@ export interface UserProps {
}
export const User = ({ navOpen, setNavOpen }: UserProps): JSX.Element => {
const { t } = useTranslation();
const [debug, setDebug] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const dispatch = useAppDispatch();

10
todo.txt

@ -4,6 +4,14 @@ add default value to undefined protobufs, (omit if default to keep them small (o
add input validation min,max etc
change ch type select to disable, make primary and delete buttons
maybe make channel editor acordion?
add url routing for settings tabs
form not updated if rendered before store populated, populated on remount, also disabled fields will remain disabled even if there disable props provided by redux are true, required toggling
add loading blur to card (prop)
form still considered dirty after save
form prefix should be located in the input (absolute?)
form suffix should focus input
reset store on new connection
meshtastic.js
- either extrapolate user and position out of nodeInfo, or fire off events for all position and user packets even when contained in a nodeInfo packet, decide how to fire events for nodeInfo, wether we merge it or do something else
- either extrapolate user and position out of nodeInfo, or fire off events for all position and user packets even when contained in a nodeInfo packet, decide how to fire events for nodeInfo, wether we merge it or do something else
- fix entering device-reconnecting state and not re-connecting despite packets being received
Loading…
Cancel
Save