Browse Source

WIP

pull/2/head
Sacha Weatherstone 5 years ago
parent
commit
074f9a7210
  1. 1
      package.json
  2. 17
      pnpm-lock.yaml
  3. 44
      src/components/Channel.tsx
  4. 305
      src/components/Connection.tsx
  5. 10
      src/components/FormFooter.tsx
  6. 115
      src/components/LoraConfig.tsx
  7. 64
      src/components/connection/BLE.tsx
  8. 40
      src/components/connection/HTTP.tsx
  9. 65
      src/components/connection/Serial.tsx
  10. 14
      src/components/generic/Modal.tsx
  11. 12
      src/components/menu/buttons/DeviceStatus.tsx
  12. 40
      src/core/connection.ts
  13. 48
      src/core/slices/appSlice.ts
  14. 202
      src/pages/settings/Channels.tsx
  15. 2
      todo.txt

1
package.json

@ -25,6 +25,7 @@
"react-i18next": "^11.14.2", "react-i18next": "^11.14.2",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0", "react-json-pretty": "^2.2.0",
"react-qr-code": "^2.0.3",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"rfc4648": "^1.5.0", "rfc4648": "^1.5.0",
"swr": "^1.0.1", "swr": "^1.0.1",

17
pnpm-lock.yaml

@ -36,6 +36,7 @@ specifiers:
react-i18next: ^11.14.2 react-i18next: ^11.14.2
react-icons: ^4.3.1 react-icons: ^4.3.1
react-json-pretty: ^2.2.0 react-json-pretty: ^2.2.0
react-qr-code: ^2.0.3
react-redux: ^7.2.6 react-redux: ^7.2.6
rfc4648: ^1.5.0 rfc4648: ^1.5.0
swr: ^1.0.1 swr: ^1.0.1
@ -63,6 +64,7 @@ dependencies:
react-i18next: 11.14[email protected][email protected] react-i18next: 11.14[email protected][email protected]
react-icons: 4.3[email protected] react-icons: 4.3[email protected]
react-json-pretty: 2.2[email protected][email protected] react-json-pretty: 2.2[email protected][email protected]
react-qr-code: 2.0[email protected]
react-redux: 7.2[email protected][email protected] react-redux: 7.2[email protected][email protected]
rfc4648: 1.5.0 rfc4648: 1.5.0
swr: 1.0[email protected] swr: 1.0[email protected]
@ -4063,6 +4065,10 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/qr.js/0.0.0:
resolution: {integrity: sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=}
dev: false
/queue-microtask/1.2.3: /queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
@ -4149,6 +4155,17 @@ packages:
react-dom: 17.0[email protected] react-dom: 17.0[email protected]
dev: false dev: false
/react-qr-code/[email protected]:
resolution: {integrity: sha512-6GDH0l53lksf2JgZwwcoS0D60a1OAal/GQRyNFkMBW19HjSqvtD5S20scmSQsKl+BgWM85Wd5DCcUYoHd+PZnQ==}
peerDependencies:
react: ^16.x || ^17.x
react-native-svg: '*'
dependencies:
prop-types: 15.7.2
qr.js: 0.0.0
react: 17.0.2
dev: false
/react-redux/[email protected][email protected]: /react-redux/[email protected][email protected]:
resolution: {integrity: sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==} resolution: {integrity: sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==}
peerDependencies: peerDependencies:

44
src/components/Channel.tsx

@ -1,16 +1,19 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FaQrcode } from 'react-icons/fa';
import { FiEdit3, FiSave } from 'react-icons/fi'; import { FiEdit3, FiSave } from 'react-icons/fi';
import QRCode from 'react-qr-code';
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 { Loading } from '@components/generic/Loading'; import { Loading } from '@components/generic/Loading';
import { Modal } from '@components/generic/Modal';
import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs'; import { Protobuf } from '@meshtastic/meshtasticjs';
import { connection } from '../core/connection';
import { Checkbox } from './generic/form/Checkbox';
import { Input } from './generic/form/Input';
import { IconButton } from './generic/IconButton';
export interface ChannelProps { export interface ChannelProps {
channel: Protobuf.Channel; channel: Protobuf.Channel;
hideEnabled?: boolean; hideEnabled?: boolean;
@ -22,6 +25,7 @@ export const Channel = ({
}: ChannelProps): JSX.Element => { }: ChannelProps): JSX.Element => {
const [edit, setEdit] = React.useState(false); const [edit, setEdit] = React.useState(false);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [showQr, setShowQr] = React.useState(false);
const { register, handleSubmit } = useForm<{ const { register, handleSubmit } = useForm<{
enabled: boolean; enabled: boolean;
@ -80,6 +84,16 @@ export const Channel = ({
return ( return (
<div className="relative flex justify-between p-3 bg-gray-100 rounded-md dark:bg-gray-700"> <div className="relative flex justify-between p-3 bg-gray-100 rounded-md dark:bg-gray-700">
<Modal
open={showQr}
onClose={(): void => {
setShowQr(false);
}}
>
<Card>
<QRCode className="rounded-md" value="test" />
</Card>
</Modal>
{edit ? ( {edit ? (
<> <>
{loading && <Loading />} {loading && <Loading />}
@ -139,12 +153,20 @@ export const Channel = ({
: `Channel: ${channel.index}`} : `Channel: ${channel.index}`}
</div> </div>
</div> </div>
<IconButton <div className="flex gap-2">
onClick={(): void => { <IconButton
setEdit(true); onClick={(): void => {
}} setShowQr(true);
icon={<FiEdit3 />} }}
/> icon={<FaQrcode />}
/>
<IconButton
onClick={(): void => {
setEdit(true);
}}
icon={<FiEdit3 />}
/>
</div>
</> </>
)} )}
</div> </div>

305
src/components/Connection.tsx

@ -1,267 +1,114 @@
import React from 'react'; import React from 'react';
import { FiCheck } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux'; import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Serial } from '@components/connection/Serial';
import { Button } from '@components/generic/Button'; import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select'; import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton';
import { Modal } from '@components/generic/Modal'; import { Modal } from '@components/generic/Modal';
import { import {
ble, cleanupListeners,
connection, connection,
connectionUrl, connectionUrl,
serial,
setConnection, setConnection,
} from '@core/connection'; } from '@core/connection';
import { closeConnectionModal } from '@core/slices/appSlice';
import { import {
IBLEConnection, closeConnectionModal,
IHTTPConnection, connType,
ISerialConnection, setConnectionParams,
Protobuf, setConnType,
SettingsManager, } from '@core/slices/appSlice';
} from '@meshtastic/meshtasticjs'; import { Types } from '@meshtastic/meshtasticjs';
import type {
BLEConnectionParameters,
HTTPConnectionParameters,
SerialConnectionParameters,
} from '@meshtastic/meshtasticjs/dist/types';
import { DeviceStatus } from './menu/buttons/DeviceStatus';
enum connType { import { BLE } from './connection/BLE';
HTTP, import { HTTP } from './connection/HTTP';
BLE,
SERIAL,
}
export const Connection = (): JSX.Element => { export const Connection = (): JSX.Element => {
const dispatch = useAppDispatch(); 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 hostOverrideEnabled = useAppSelector(
(state) => state.meshtastic.hostOverrideEnabled,
);
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
const connectionModalOpen = useAppSelector(
(state) => state.app.connectionModalOpen,
);
const ready = useAppSelector((state) => state.meshtastic.ready);
const connect = async (
connectionType: connType,
params:
| HTTPConnectionParameters
| SerialConnectionParameters
| BLEConnectionParameters,
): Promise<void> => {
connection.complete();
await connection.disconnect();
if (connectionType === connType.BLE) {
setConnection(new IBLEConnection());
} else if (connectionType === connType.HTTP) {
setConnection(new IHTTPConnection());
} else {
setConnection(new ISerialConnection());
}
// @ts-ignore tmp
await connection.connect(params);
};
const updateBleDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await ble.getDevices();
setBleDevices(devices);
}, []);
const updateSerialDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await serial.getPorts();
setSerialDevices(devices);
}, []);
React.useEffect(() => {
if (ready) {
dispatch(closeConnectionModal());
}
}, [ready, dispatch]);
React.useEffect(() => { const state = useAppSelector((state) => state.meshtastic);
if (selectedConnType === connType.BLE) { const appState = useAppSelector((state) => state.app);
void updateBleDeviceList();
}
if (selectedConnType === connType.SERIAL) {
void updateSerialDeviceList();
}
}, [selectedConnType, updateBleDeviceList, updateSerialDeviceList]);
React.useEffect(() => { React.useEffect(() => {
const connectionMethod = localStorage.getItem('connectionMethod'); dispatch(
setConnectionParams({
switch (connectionMethod) { type: connType.HTTP,
case 'serial': params: {
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, address: connectionUrl,
tls: false, tls: false,
receiveBatchRequests: false, receiveBatchRequests: false,
fetchInterval: 2000, fetchInterval: 2000,
}); },
break; }),
);
void setConnection(connType.HTTP);
}, [dispatch]);
React.useEffect(() => {
if (state.ready) {
dispatch(closeConnectionModal());
} }
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE; }, [state.ready, dispatch]);
}, [hostOverrideEnabled, hostOverride]);
return ( return (
<Modal <Modal
open={connectionModalOpen} className="w-full max-w-3xl"
open={appState.connectionModalOpen}
// open={true}
onClose={(): void => { onClose={(): void => {
dispatch(closeConnectionModal()); dispatch(closeConnectionModal());
}} }}
> >
<Card> <Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl"> <div className="w-full max-w-3xl p-10">
{ready ? ( <div className="flex justify-between w-full bg-gray-100 rounded-md">
<form className="space-y-2"> <div className="p-2">
<Select <h1>
label="Method" {`Connected to: ${
optionsEnum={connType} state.nodes.find(
value={selectedConnType} (node) => node.number === state.radio.hardware.myNodeNum,
onChange={(e): void => { )?.user?.longName ?? 'Unknown'
setSelectedConnType(parseInt(e.target.value)); }`}
}} </h1>
/> <p>{`Via: ${connType[appState.connType]}`}</p>
{selectedConnType === connType.HTTP && ( </div>
<> <div className="p-2 my-auto">
<Select {state.deviceStatus ===
label="Host Source" Types.DeviceStatusEnum.DEVICE_DISCONNECTED ? (
options={[ <Button
{ border
name: 'Local', onClick={async (): Promise<void> => {
value: 'local', await setConnection();
}, }}
{ >
name: 'Remote', Connect
value: 'remote', </Button>
}, ) : (
]} <Button
value={httpIpSource} border
onChange={(e): void => { onClick={async (): Promise<void> => {
setHttpIpSource(e.target.value as 'local' | 'remote'); await connection.disconnect();
}} cleanupListeners();
/> }}
{httpIpSource === 'local' ? ( >
<Input label="Host" value={connectionUrl} disabled /> Disconnect
) : ( </Button>
<Input label="Host" />
)}
<Checkbox label="Use TLS?" />
</>
)}
{selectedConnType === connType.BLE && (
<div>
<div className="flex space-x-2">
<Button border onClick={updateBleDeviceList}>
Refresh List
</Button>
<Button
border
onClick={async (): Promise<void> => {
await ble.getDevice();
}}
>
New Device
</Button>
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{bleDevices.map((device, index) => (
<div
onClick={async (): Promise<void> => {
await connect(connType.BLE, {
device: device,
});
}}
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={index}
>
<div className="my-auto">{device.name}</div>
<IconButton
onClick={async (): Promise<void> => {
await connect(connType.BLE, {
device: device,
});
}}
icon={<FiCheck />}
/>
</div>
))}
</div>
</div>
)}
{selectedConnType === connType.SERIAL && (
<div>
<div className="flex space-x-2">
<Button border onClick={updateSerialDeviceList}>
Refresh List
</Button>
<Button
border
onClick={async (): Promise<void> => {
console.log(await serial.getPort());
}}
>
New Device
</Button>
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{serialDevices.map((device, index) => (
<div
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={index}
>
<div className="my-auto">
{device.getInfo().usbProductId}
{device.getInfo().usbVendorId}
</div>
<IconButton
onClick={async (): Promise<void> => {
await connect(connType.SERIAL, {
// @ts-ignore tmp
device: device,
});
}}
icon={<FiCheck />}
/>
<JSONPretty data={device.getInfo()} />
</div>
))}
</div>
</div>
)} )}
</form>
) : (
<div>
<DeviceStatus />
</div> </div>
)} </div>
<form className="space-y-2">
<Select
label="Method"
optionsEnum={connType}
value={appState.connType}
onChange={(e): void => {
dispatch(setConnType(e.target.value as unknown as connType));
}}
/>
{appState.connType === connType.HTTP && <HTTP />}
{appState.connType === connType.BLE && <BLE />}
{appState.connType === connType.SERIAL && <Serial />}
</form>
</div> </div>
</Card> </Card>
</Modal> </Modal>

10
src/components/FormFooter.tsx

@ -5,9 +5,9 @@ import { FiSave, FiXCircle } from 'react-icons/fi';
import { IconButton } from './generic/IconButton'; import { IconButton } from './generic/IconButton';
export interface FormFooterProps { export interface FormFooterProps {
dirty: boolean; dirty?: boolean;
clearAction: () => void; clearAction?: () => void;
saveAction: () => void; saveAction?: () => void;
} }
export const FormFooter = ({ export const FormFooter = ({
@ -21,13 +21,13 @@ export const FormFooter = ({
icon={<FiXCircle className="w-5 h-5" />} icon={<FiXCircle className="w-5 h-5" />}
disabled={!dirty} disabled={!dirty}
onClick={(): void => { onClick={(): void => {
clearAction(); clearAction && clearAction();
}} }}
/> />
<IconButton <IconButton
disabled={!dirty} disabled={!dirty}
onClick={(): void => { onClick={(): void => {
saveAction(); saveAction && saveAction();
}} }}
icon={<FiSave className="w-5 h-5" />} icon={<FiSave className="w-5 h-5" />}
/> />

115
src/components/LoraConfig.tsx

@ -1,115 +0,0 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Loading } from '@components/generic/Loading';
import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs';
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>
);
};

64
src/components/connection/BLE.tsx

@ -0,0 +1,64 @@
import React from 'react';
import { FiCheck } from 'react-icons/fi';
import { connType } from '@app/core/slices/appSlice';
import { Button } from '@components/generic/Button';
import { IconButton } from '@components/generic/IconButton';
import { ble, setConnection } from '@core/connection';
export const BLE = (): JSX.Element => {
const [bleDevices, setBleDevices] = React.useState<BluetoothDevice[]>([]);
const updateBleDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await ble.getDevices();
setBleDevices(devices);
}, []);
React.useEffect(() => {
void updateBleDeviceList();
}, [updateBleDeviceList]);
return (
<div>
<div className="flex space-x-2">
<Button type="button" border onClick={updateBleDeviceList}>
Refresh List
</Button>
<Button
type="button"
border
onClick={async (): Promise<void> => {
await ble.getDevice();
}}
>
New Device
</Button>
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{bleDevices.map((device, index) => (
<div
onClick={async (): Promise<void> => {
await setConnection(connType.BLE, {
device: device,
});
}}
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={index}
>
<div className="my-auto">{device.name}</div>
<IconButton
onClick={async (): Promise<void> => {
await setConnection(connType.BLE, {
device: device,
});
}}
icon={<FiCheck />}
/>
</div>
))}
</div>
</div>
);
};

40
src/components/connection/HTTP.tsx

@ -0,0 +1,40 @@
import React from 'react';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connectionUrl } from '@core/connection';
export const HTTP = (): JSX.Element => {
const [httpIpSource, setHttpIpSource] = React.useState<'local' | 'remote'>(
'local',
);
return (
<div>
<Select
label="Host Source"
options={[
{
name: 'Local',
value: 'local',
},
{
name: 'Remote',
value: 'remote',
},
]}
value={httpIpSource}
onChange={(e): void => {
setHttpIpSource(e.target.value as 'local' | 'remote');
}}
/>
{httpIpSource === 'local' ? (
<Input label="Host" value={connectionUrl} disabled />
) : (
<Input label="Host" />
)}
<Checkbox label="Use TLS?" />
</div>
);
};

65
src/components/connection/Serial.tsx

@ -0,0 +1,65 @@
import React from 'react';
import { FiCheck } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { Button } from '@components/generic/Button';
import { IconButton } from '@components/generic/IconButton';
import { serial, setConnection } from '@core/connection';
import { connType } from '@core/slices/appSlice';
export const Serial = (): JSX.Element => {
const [serialDevices, setSerialDevices] = React.useState<SerialPort[]>([]);
const updateSerialDeviceList = React.useCallback(async (): Promise<void> => {
const devices = await serial.getPorts();
setSerialDevices(devices);
}, []);
React.useEffect(() => {
void updateSerialDeviceList();
}, [updateSerialDeviceList]);
return (
<div>
<div className="flex space-x-2">
<Button type="button" border onClick={updateSerialDeviceList}>
Refresh List
</Button>
<Button
type="button"
border
onClick={async (): Promise<void> => {
console.log(await serial.getPort());
}}
>
New Device
</Button>
</div>
<div className="space-y-2">
<div>Previously connected devices</div>
{serialDevices.map((device, index) => (
<div
className="flex justify-between p-2 bg-gray-700 rounded-md"
key={index}
>
<div className="my-auto">
{device.getInfo().usbProductId}
{device.getInfo().usbVendorId}
</div>
<IconButton
onClick={async (): Promise<void> => {
await setConnection(connType.SERIAL, {
// @ts-ignore tmp
device: device,
});
}}
icon={<FiCheck />}
/>
<JSONPretty data={device.getInfo()} />
</div>
))}
</div>
</div>
);
};

14
src/components/generic/Modal.tsx

@ -3,13 +3,21 @@ import type React from 'react';
import { useAppSelector } from '@app/hooks/redux'; import { useAppSelector } from '@app/hooks/redux';
import { Dialog } from '@headlessui/react'; import { Dialog } from '@headlessui/react';
export interface ModalProps { type DefaultDivProps = JSX.IntrinsicElements['div'];
export interface ModalProps extends DefaultDivProps {
children: React.ReactNode; children: React.ReactNode;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
} }
export const Modal = ({ children, open, onClose }: ModalProps): JSX.Element => { export const Modal = ({
children,
open,
onClose,
className,
...props
}: ModalProps): JSX.Element => {
const darkMode = useAppSelector((state) => state.app.darkMode); const darkMode = useAppSelector((state) => state.app.darkMode);
return ( return (
<> <>
@ -27,7 +35,7 @@ export const Modal = ({ children, open, onClose }: ModalProps): JSX.Element => {
> >
&#8203; &#8203;
</span> </span>
<div className="inline-block w-full max-w-3xl align-middle"> <div className={`inline-block align-middle ${className}`} {...props}>
{children} {children}
</div> </div>
</div> </div>

12
src/components/menu/buttons/DeviceStatus.tsx

@ -9,7 +9,7 @@ import { Types } from '@meshtastic/meshtasticjs';
export const DeviceStatus = (): JSX.Element => { export const DeviceStatus = (): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus); const state = useAppSelector((state) => state.meshtastic);
const ready = useAppSelector((state) => state.meshtastic.ready); const ready = useAppSelector((state) => state.meshtastic.ready);
return ( return (
@ -27,18 +27,22 @@ export const DeviceStatus = (): JSX.Element => {
[ [
Types.DeviceStatusEnum.DEVICE_CONNECTED, Types.DeviceStatusEnum.DEVICE_CONNECTED,
Types.DeviceStatusEnum.DEVICE_CONFIGURED, Types.DeviceStatusEnum.DEVICE_CONFIGURED,
].includes(deviceStatus) ].includes(state.deviceStatus)
? 'bg-green-400' ? 'bg-green-400'
: [ : [
Types.DeviceStatusEnum.DEVICE_CONNECTING, Types.DeviceStatusEnum.DEVICE_CONNECTING,
Types.DeviceStatusEnum.DEVICE_RECONNECTING, Types.DeviceStatusEnum.DEVICE_RECONNECTING,
Types.DeviceStatusEnum.DEVICE_CONFIGURING, Types.DeviceStatusEnum.DEVICE_CONFIGURING,
].includes(deviceStatus) ].includes(state.deviceStatus)
? 'bg-yellow-400' ? 'bg-yellow-400'
: 'bg-gray-400' : 'bg-gray-400'
}`} }`}
></div> ></div>
<div className="my-auto">{Types.DeviceStatusEnum[deviceStatus]}</div> <div className="my-auto">
{state.nodes.find(
(node) => node.number === state.radio.hardware.myNodeNum,
)?.user?.longName ?? 'Unknown'}
</div>
<div className="py-2"> <div className="py-2">
{ready ? ( {ready ? (
<FiWifi className="w-5 h-5" /> <FiWifi className="w-5 h-5" />

40
src/core/connection.ts

@ -1,3 +1,4 @@
import { connType } from '@core/slices/appSlice';
import { import {
addChannel, addChannel,
addMessage, addMessage,
@ -16,6 +17,7 @@ import {
IHTTPConnection, IHTTPConnection,
ISerialConnection, ISerialConnection,
Protobuf, Protobuf,
SettingsManager,
Types, Types,
} from '@meshtastic/meshtasticjs'; } from '@meshtastic/meshtasticjs';
@ -34,14 +36,42 @@ export const connectionUrl = state.hostOverrideEnabled
export const ble = new IBLEConnection(); export const ble = new IBLEConnection();
export const serial = new ISerialConnection(); export const serial = new ISerialConnection();
export const setConnection = (conn: connectionType): void => { export const setConnection = async (conn: connType): Promise<void> => {
await connection.disconnect();
cleanupListeners(); cleanupListeners();
connection = conn; switch (conn) {
case connType.HTTP:
connection = new IHTTPConnection();
break;
case connType.BLE:
connection = new IBLEConnection();
break;
case connType.SERIAL:
connection = new ISerialConnection();
break;
}
registerListeners(); registerListeners();
const connectionParams = store.getState().app.connectionParams;
switch (conn) {
case connType.HTTP:
await connection.connect(connectionParams.HTTP);
break;
case connType.BLE:
await connection.connect(
// @ts-ignore tmp
connectionParams.BLE,
);
break;
case connType.SERIAL:
await connection.connect(
// @ts-ignore tmp
connectionParams.SERIAL,
);
break;
}
}; };
const cleanupListeners = (): void => { export const cleanupListeners = (): void => {
connection.onDeviceStatus.cancelAll(); connection.onDeviceStatus.cancelAll();
connection.onMyNodeInfo.cancelAll(); connection.onMyNodeInfo.cancelAll();
connection.onUserPacket.cancelAll(); connection.onUserPacket.cancelAll();
@ -53,6 +83,8 @@ const cleanupListeners = (): void => {
}; };
const registerListeners = (): void => { const registerListeners = (): void => {
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
connection.onDeviceStatus.subscribe((status) => { connection.onDeviceStatus.subscribe((status) => {
store.dispatch(setDeviceStatus(status)); store.dispatch(setDeviceStatus(status));

48
src/core/slices/appSlice.ts

@ -1,13 +1,26 @@
import type { Types } from '@meshtastic/meshtasticjs';
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
export type currentPageName = 'messages' | 'settings'; export type currentPageName = 'messages' | 'settings';
export enum connType {
HTTP,
BLE,
SERIAL,
}
interface AppState { interface AppState {
mobileNavOpen: boolean; mobileNavOpen: boolean;
connectionModalOpen: boolean; connectionModalOpen: boolean;
darkMode: boolean; darkMode: boolean;
currentPage: currentPageName; currentPage: currentPageName;
connType: connType;
connectionParams: {
BLE: Types.BLEConnectionParameters;
HTTP: Types.HTTPConnectionParameters;
SERIAL: Types.SerialConnectionParameters;
};
} }
const initialState: AppState = { const initialState: AppState = {
@ -15,6 +28,17 @@ const initialState: AppState = {
connectionModalOpen: true, connectionModalOpen: true,
darkMode: localStorage.getItem('darkMode') === 'true' ?? false, darkMode: localStorage.getItem('darkMode') === 'true' ?? false,
currentPage: 'messages', currentPage: 'messages',
connType: connType.HTTP,
connectionParams: {
BLE: {},
HTTP: {
address: 'http://meshtastic.local/',
tls: false,
receiveBatchRequests: false,
fetchInterval: 2000,
},
SERIAL: {},
},
}; };
export const appSlice = createSlice({ export const appSlice = createSlice({
@ -40,6 +64,28 @@ export const appSlice = createSlice({
setCurrentPage(state, action: PayloadAction<currentPageName>) { setCurrentPage(state, action: PayloadAction<currentPageName>) {
state.currentPage = action.payload; state.currentPage = action.payload;
}, },
setConnType(state, action: PayloadAction<connType>) {
state.connType = action.payload;
},
setConnectionParams(
state,
action: PayloadAction<{
type: connType;
params: Types.ConnectionParameters;
}>,
) {
switch (action.payload.type) {
case connType.BLE:
state.connectionParams.BLE = action.payload.params;
break;
case connType.HTTP:
state.connectionParams.HTTP = action.payload.params;
break;
case connType.SERIAL:
state.connectionParams.SERIAL = action.payload.params;
break;
}
},
}, },
}); });
@ -50,6 +96,8 @@ export const {
closeConnectionModal, closeConnectionModal,
setDarkModeEnabled, setDarkModeEnabled,
setCurrentPage, setCurrentPage,
setConnType,
setConnectionParams,
} = appSlice.actions; } = appSlice.actions;
export default appSlice.reducer; export default appSlice.reducer;

202
src/pages/settings/Channels.tsx

@ -1,17 +1,23 @@
import React from 'react'; import React from 'react';
import { FiCode, FiMenu, FiSave } from 'react-icons/fi'; import { useForm, useWatch } from 'react-hook-form';
import { FiCode, FiMenu } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/redux'; import { useAppSelector } from '@app/hooks/redux';
import { Channel } from '@components/Channel'; import { Channel } from '@components/Channel';
import { FormFooter } from '@components/FormFooter';
import { Button } from '@components/generic/Button'; import { Button } from '@components/generic/Button';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { Cover } from '@components/generic/Cover'; import { Cover } from '@components/generic/Cover';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { IconButton } from '@components/generic/IconButton'; import { IconButton } from '@components/generic/IconButton';
import { LoraConfig } from '@components/LoraConfig'; import { Loading } from '@components/generic/Loading';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface ChannelsProps { export interface ChannelsProps {
navOpen?: boolean; navOpen?: boolean;
@ -23,7 +29,122 @@ export const Channels = ({
setNavOpen, setNavOpen,
}: ChannelsProps): JSX.Element => { }: ChannelsProps): JSX.Element => {
const channels = useAppSelector((state) => state.meshtastic.radio.channels); const channels = useAppSelector((state) => state.meshtastic.radio.channels);
const channel = channels[0].channel;
const [debug, setDebug] = React.useState(false); const [debug, setDebug] = React.useState(false);
const [loading, setLoading] = React.useState(false);
enum PresetName {
'Long Slow',
'Long Alt',
'Medium',
'Short Fast',
}
const { register, handleSubmit, reset, formState, control } = useForm<{
simple: boolean;
preset?: PresetName;
enabled: boolean;
settings: {
name: string;
bandwidth?: number;
codingRate?: number;
spreadFactor?: number;
downlinkEnabled?: boolean;
uplinkEnabled?: boolean;
txPower?: number;
psk?: string;
};
}>({
defaultValues: {
simple: true,
preset: PresetName['Long Slow'],
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 presets = [
{
name: PresetName['Long Slow'],
config: {
bandwidth: 125,
codingRate: 8, // 4/8
spreadFactor: 12, // 4096chips/symbol
},
},
{
name: PresetName['Long Alt'],
config: {
bandwidth: 31.25,
codingRate: 8, // 4/8
spreadFactor: 9, // 512chips/symbol,
},
},
{
name: PresetName['Medium'],
config: {
bandwidth: 125,
codingRate: 5, // 4/5
spreadFactor: 7, // 128chips/symbol,
},
},
{
name: PresetName['Short Fast'],
config: {
bandwidth: 500,
codingRate: 5, // 4/5
spreadFactor: 7, // 128chips/symbol,
},
},
];
const watchSimple = useWatch({
control,
name: 'simple',
defaultValue: true,
});
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
console.log(data);
const selectedPreset = data.simple
? presets.find((preset) => preset.name === data.preset)?.config
: undefined;
const adminChannel = Protobuf.Channel.create({
role: data.enabled
? Protobuf.Channel_Role.SECONDARY
: Protobuf.Channel_Role.DISABLED,
index: channel.index,
settings: {
...data.settings,
...selectedPreset,
psk: new TextEncoder().encode(data.settings.psk),
},
});
console.log(adminChannel);
// await connection.setChannel(adminChannel, (): Promise<void> => {
// setLoading(false);
// return Promise.resolve();
// });
});
return ( return (
<PrimaryTemplate <PrimaryTemplate
@ -47,18 +168,77 @@ export const Channels = ({
/> />
} }
footer={ footer={
<Button <FormFooter
className="px-10 ml-auto" dirty={formState.isDirty}
icon={<FiSave className="w-5 h-5" />} saveAction={onSubmit}
active clearAction={reset}
border />
>
Confirm
</Button>
} }
> >
<div className="space-y-4"> <div className="space-y-4">
{channels[0] && <LoraConfig channel={channels[0].channel} />} {channel && (
<Card>
{loading && <Loading />}
<div className="w-full max-w-3xl p-10 md:max-w-xl">
{/* TODO: get gap working */}
<Checkbox
label="Use Presets"
{...register('simple')}
// checked={simpleChannelSettings}
// onChange={(e): void =>
// setSimpleChannelSettings(e.target.checked)
// }
/>
<form onSubmit={onSubmit}>
{watchSimple ? (
<Select label="Preset" optionsEnum={PresetName} />
) : (
<>
<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>
)}
<Card> <Card>
<Cover enabled={debug} content={<JSONPretty data={channels} />} /> <Cover enabled={debug} content={<JSONPretty data={channels} />} />
<div className="w-full p-4 space-y-2 md:p-10"> <div className="w-full p-4 space-y-2 md:p-10">

2
todo.txt

@ -9,6 +9,8 @@ form prefix should be located in the input (absolute?)
form suffix should focus input form suffix should focus input
reset store on new connection reset store on new connection
redux actions seem to be dispatched twice redux actions seem to be dispatched twice
add qr generator in channel editor
no save button for channel config (bw,sf,cr,tx etc)
meshtastic.js meshtastic.js
- fix entering device-reconnecting state and not re-connecting despite packets being received - fix entering device-reconnecting state and not re-connecting despite packets being received
Loading…
Cancel
Save