WIP
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
Generated
+17
@@ -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.2_i18next@21.5.2+react@17.0.2
|
react-i18next: 11.14.2_i18next@21.5.2+react@17.0.2
|
||||||
react-icons: 4.3.1_react@17.0.2
|
react-icons: 4.3.1_react@17.0.2
|
||||||
react-json-pretty: 2.2.0_react-dom@17.0.2+react@17.0.2
|
react-json-pretty: 2.2.0_react-dom@17.0.2+react@17.0.2
|
||||||
|
react-qr-code: 2.0.3_react@17.0.2
|
||||||
react-redux: 7.2.6_react-dom@17.0.2+react@17.0.2
|
react-redux: 7.2.6_react-dom@17.0.2+react@17.0.2
|
||||||
rfc4648: 1.5.0
|
rfc4648: 1.5.0
|
||||||
swr: 1.0.1_react@17.0.2
|
swr: 1.0.1_react@17.0.2
|
||||||
@@ -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.2_react@17.0.2
|
react-dom: 17.0.2_react@17.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-qr-code/2.0.3_react@17.0.2:
|
||||||
|
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/7.2.6_react-dom@17.0.2+react@17.0.2:
|
/react-redux/7.2.6_react-dom@17.0.2+react@17.0.2:
|
||||||
resolution: {integrity: sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==}
|
resolution: {integrity: sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
+33
-11
@@ -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>
|
||||||
|
|||||||
+77
-230
@@ -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';
|
import { BLE } from './connection/BLE';
|
||||||
|
import { HTTP } from './connection/HTTP';
|
||||||
enum connType {
|
|
||||||
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) {
|
const state = useAppSelector((state) => state.meshtastic);
|
||||||
setConnection(new IBLEConnection());
|
const appState = useAppSelector((state) => state.app);
|
||||||
} 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(() => {
|
React.useEffect(() => {
|
||||||
if (ready) {
|
dispatch(
|
||||||
dispatch(closeConnectionModal());
|
setConnectionParams({
|
||||||
}
|
type: connType.HTTP,
|
||||||
}, [ready, dispatch]);
|
params: {
|
||||||
|
|
||||||
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({
|
|
||||||
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 && (
|
|
||||||
<>
|
|
||||||
<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>
|
</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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -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" />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 => {
|
|||||||
>
|
>
|
||||||
​
|
​
|
||||||
</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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
+36
-4
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+191
-11
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user