This commit is contained in:
Sacha Weatherstone
2021-11-21 23:45:56 +11:00
parent 0cea0b2165
commit 074f9a7210
15 changed files with 598 additions and 383 deletions
+33 -11
View File
@@ -1,16 +1,19 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { FaQrcode } from 'react-icons/fa';
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 { Modal } from '@components/generic/Modal';
import { connection } from '@core/connection';
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 {
channel: Protobuf.Channel;
hideEnabled?: boolean;
@@ -22,6 +25,7 @@ export const Channel = ({
}: ChannelProps): JSX.Element => {
const [edit, setEdit] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [showQr, setShowQr] = React.useState(false);
const { register, handleSubmit } = useForm<{
enabled: boolean;
@@ -80,6 +84,16 @@ export const Channel = ({
return (
<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 ? (
<>
{loading && <Loading />}
@@ -139,12 +153,20 @@ export const Channel = ({
: `Channel: ${channel.index}`}
</div>
</div>
<IconButton
onClick={(): void => {
setEdit(true);
}}
icon={<FiEdit3 />}
/>
<div className="flex gap-2">
<IconButton
onClick={(): void => {
setShowQr(true);
}}
icon={<FaQrcode />}
/>
<IconButton
onClick={(): void => {
setEdit(true);
}}
icon={<FiEdit3 />}
/>
</div>
</>
)}
</div>
+77 -230
View File
@@ -1,267 +1,114 @@
import React from 'react';
import { FiCheck } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Serial } from '@components/connection/Serial';
import { Button } from '@components/generic/Button';
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 { IconButton } from '@components/generic/IconButton';
import { Modal } from '@components/generic/Modal';
import {
ble,
cleanupListeners,
connection,
connectionUrl,
serial,
setConnection,
} from '@core/connection';
import { closeConnectionModal } from '@core/slices/appSlice';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
SettingsManager,
} from '@meshtastic/meshtasticjs';
import type {
BLEConnectionParameters,
HTTPConnectionParameters,
SerialConnectionParameters,
} from '@meshtastic/meshtasticjs/dist/types';
closeConnectionModal,
connType,
setConnectionParams,
setConnType,
} from '@core/slices/appSlice';
import { Types } from '@meshtastic/meshtasticjs';
import { DeviceStatus } from './menu/buttons/DeviceStatus';
enum connType {
HTTP,
BLE,
SERIAL,
}
import { BLE } from './connection/BLE';
import { HTTP } from './connection/HTTP';
export const Connection = (): 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 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);
}, []);
const state = useAppSelector((state) => state.meshtastic);
const appState = useAppSelector((state) => state.app);
React.useEffect(() => {
if (ready) {
dispatch(closeConnectionModal());
}
}, [ready, dispatch]);
React.useEffect(() => {
if (selectedConnType === connType.BLE) {
void updateBleDeviceList();
}
if (selectedConnType === connType.SERIAL) {
void updateSerialDeviceList();
}
}, [selectedConnType, updateBleDeviceList, updateSerialDeviceList]);
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({
dispatch(
setConnectionParams({
type: connType.HTTP,
params: {
address: connectionUrl,
tls: false,
receiveBatchRequests: false,
fetchInterval: 2000,
});
break;
},
}),
);
void setConnection(connType.HTTP);
}, [dispatch]);
React.useEffect(() => {
if (state.ready) {
dispatch(closeConnectionModal());
}
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
}, [hostOverrideEnabled, hostOverride]);
}, [state.ready, dispatch]);
return (
<Modal
open={connectionModalOpen}
className="w-full max-w-3xl"
open={appState.connectionModalOpen}
// open={true}
onClose={(): void => {
dispatch(closeConnectionModal());
}}
>
<Card>
<div className="w-full max-w-3xl p-10 md:max-w-xl">
{ready ? (
<form className="space-y-2">
<Select
label="Method"
optionsEnum={connType}
value={selectedConnType}
onChange={(e): void => {
setSelectedConnType(parseInt(e.target.value));
}}
/>
{selectedConnType === connType.HTTP && (
<>
<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?" />
</>
)}
{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 className="w-full max-w-3xl p-10">
<div className="flex justify-between w-full bg-gray-100 rounded-md">
<div className="p-2">
<h1>
{`Connected to: ${
state.nodes.find(
(node) => node.number === state.radio.hardware.myNodeNum,
)?.user?.longName ?? 'Unknown'
}`}
</h1>
<p>{`Via: ${connType[appState.connType]}`}</p>
</div>
)}
<div className="p-2 my-auto">
{state.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_DISCONNECTED ? (
<Button
border
onClick={async (): Promise<void> => {
await setConnection();
}}
>
Connect
</Button>
) : (
<Button
border
onClick={async (): Promise<void> => {
await connection.disconnect();
cleanupListeners();
}}
>
Disconnect
</Button>
)}
</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>
</Card>
</Modal>
+5 -5
View File
@@ -5,9 +5,9 @@ import { FiSave, FiXCircle } from 'react-icons/fi';
import { IconButton } from './generic/IconButton';
export interface FormFooterProps {
dirty: boolean;
clearAction: () => void;
saveAction: () => void;
dirty?: boolean;
clearAction?: () => void;
saveAction?: () => void;
}
export const FormFooter = ({
@@ -21,13 +21,13 @@ export const FormFooter = ({
icon={<FiXCircle className="w-5 h-5" />}
disabled={!dirty}
onClick={(): void => {
clearAction();
clearAction && clearAction();
}}
/>
<IconButton
disabled={!dirty}
onClick={(): void => {
saveAction();
saveAction && saveAction();
}}
icon={<FiSave className="w-5 h-5" />}
/>
-115
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
};
+11 -3
View File
@@ -3,13 +3,21 @@ import type React from 'react';
import { useAppSelector } from '@app/hooks/redux';
import { Dialog } from '@headlessui/react';
export interface ModalProps {
type DefaultDivProps = JSX.IntrinsicElements['div'];
export interface ModalProps extends DefaultDivProps {
children: React.ReactNode;
open: boolean;
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);
return (
<>
@@ -27,7 +35,7 @@ export const Modal = ({ children, open, onClose }: ModalProps): JSX.Element => {
>
&#8203;
</span>
<div className="inline-block w-full max-w-3xl align-middle">
<div className={`inline-block align-middle ${className}`} {...props}>
{children}
</div>
</div>
+8 -4
View File
@@ -9,7 +9,7 @@ import { Types } from '@meshtastic/meshtasticjs';
export const DeviceStatus = (): JSX.Element => {
const dispatch = useAppDispatch();
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus);
const state = useAppSelector((state) => state.meshtastic);
const ready = useAppSelector((state) => state.meshtastic.ready);
return (
@@ -27,18 +27,22 @@ export const DeviceStatus = (): JSX.Element => {
[
Types.DeviceStatusEnum.DEVICE_CONNECTED,
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
].includes(deviceStatus)
].includes(state.deviceStatus)
? 'bg-green-400'
: [
Types.DeviceStatusEnum.DEVICE_CONNECTING,
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
].includes(deviceStatus)
].includes(state.deviceStatus)
? 'bg-yellow-400'
: 'bg-gray-400'
}`}
></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">
{ready ? (
<FiWifi className="w-5 h-5" />