mirror of
https://github.com/meshtastic/web.git
synced 2026-05-19 11:45:17 -04:00
WIP
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
"react-i18next": "^11.14.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-json-pretty": "^2.2.0",
|
||||
"react-qr-code": "^2.0.3",
|
||||
"react-redux": "^7.2.6",
|
||||
"rfc4648": "^1.5.0",
|
||||
"swr": "^1.0.1",
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -36,6 +36,7 @@ specifiers:
|
||||
react-i18next: ^11.14.2
|
||||
react-icons: ^4.3.1
|
||||
react-json-pretty: ^2.2.0
|
||||
react-qr-code: ^2.0.3
|
||||
react-redux: ^7.2.6
|
||||
rfc4648: ^1.5.0
|
||||
swr: ^1.0.1
|
||||
@@ -63,6 +64,7 @@ dependencies:
|
||||
react-i18next: 11.14.2_i18next@21.5.2+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-qr-code: 2.0.3_react@17.0.2
|
||||
react-redux: 7.2.6_react-dom@17.0.2+react@17.0.2
|
||||
rfc4648: 1.5.0
|
||||
swr: 1.0.1_react@17.0.2
|
||||
@@ -4063,6 +4065,10 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/qr.js/0.0.0:
|
||||
resolution: {integrity: sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=}
|
||||
dev: false
|
||||
|
||||
/queue-microtask/1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
@@ -4149,6 +4155,17 @@ packages:
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
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:
|
||||
resolution: {integrity: sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==}
|
||||
peerDependencies:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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" />}
|
||||
/>
|
||||
|
||||
@@ -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
Normal file
64
src/components/connection/BLE.tsx
Normal 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
src/components/connection/HTTP.tsx
Normal file
40
src/components/connection/HTTP.tsx
Normal 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
src/components/connection/Serial.tsx
Normal file
65
src/components/connection/Serial.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 => {
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<div className="inline-block w-full max-w-3xl align-middle">
|
||||
<div className={`inline-block align-middle ${className}`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { connType } from '@core/slices/appSlice';
|
||||
import {
|
||||
addChannel,
|
||||
addMessage,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
IHTTPConnection,
|
||||
ISerialConnection,
|
||||
Protobuf,
|
||||
SettingsManager,
|
||||
Types,
|
||||
} from '@meshtastic/meshtasticjs';
|
||||
|
||||
@@ -34,14 +36,42 @@ export const connectionUrl = state.hostOverrideEnabled
|
||||
export const ble = new IBLEConnection();
|
||||
export const serial = new ISerialConnection();
|
||||
|
||||
export const setConnection = (conn: connectionType): void => {
|
||||
export const setConnection = async (conn: connType): Promise<void> => {
|
||||
await connection.disconnect();
|
||||
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();
|
||||
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.onMyNodeInfo.cancelAll();
|
||||
connection.onUserPacket.cancelAll();
|
||||
@@ -53,6 +83,8 @@ const cleanupListeners = (): void => {
|
||||
};
|
||||
|
||||
const registerListeners = (): void => {
|
||||
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
|
||||
|
||||
connection.onDeviceStatus.subscribe((status) => {
|
||||
store.dispatch(setDeviceStatus(status));
|
||||
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import type { Types } from '@meshtastic/meshtasticjs';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
export type currentPageName = 'messages' | 'settings';
|
||||
|
||||
export enum connType {
|
||||
HTTP,
|
||||
BLE,
|
||||
SERIAL,
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
mobileNavOpen: boolean;
|
||||
connectionModalOpen: boolean;
|
||||
darkMode: boolean;
|
||||
currentPage: currentPageName;
|
||||
connType: connType;
|
||||
connectionParams: {
|
||||
BLE: Types.BLEConnectionParameters;
|
||||
HTTP: Types.HTTPConnectionParameters;
|
||||
SERIAL: Types.SerialConnectionParameters;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: AppState = {
|
||||
@@ -15,6 +28,17 @@ const initialState: AppState = {
|
||||
connectionModalOpen: true,
|
||||
darkMode: localStorage.getItem('darkMode') === 'true' ?? false,
|
||||
currentPage: 'messages',
|
||||
connType: connType.HTTP,
|
||||
connectionParams: {
|
||||
BLE: {},
|
||||
HTTP: {
|
||||
address: 'http://meshtastic.local/',
|
||||
tls: false,
|
||||
receiveBatchRequests: false,
|
||||
fetchInterval: 2000,
|
||||
},
|
||||
SERIAL: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
@@ -40,6 +64,28 @@ export const appSlice = createSlice({
|
||||
setCurrentPage(state, action: PayloadAction<currentPageName>) {
|
||||
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,
|
||||
setDarkModeEnabled,
|
||||
setCurrentPage,
|
||||
setConnType,
|
||||
setConnectionParams,
|
||||
} = appSlice.actions;
|
||||
|
||||
export default appSlice.reducer;
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
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 { useAppSelector } from '@app/hooks/redux';
|
||||
import { Channel } from '@components/Channel';
|
||||
import { FormFooter } from '@components/FormFooter';
|
||||
import { Button } from '@components/generic/Button';
|
||||
import { Card } from '@components/generic/Card';
|
||||
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 { LoraConfig } from '@components/LoraConfig';
|
||||
import { Loading } from '@components/generic/Loading';
|
||||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||
import { connection } from '@core/connection';
|
||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export interface ChannelsProps {
|
||||
navOpen?: boolean;
|
||||
@@ -23,7 +29,122 @@ export const Channels = ({
|
||||
setNavOpen,
|
||||
}: ChannelsProps): JSX.Element => {
|
||||
const channels = useAppSelector((state) => state.meshtastic.radio.channels);
|
||||
const channel = channels[0].channel;
|
||||
|
||||
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 (
|
||||
<PrimaryTemplate
|
||||
@@ -47,18 +168,77 @@ export const Channels = ({
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<Button
|
||||
className="px-10 ml-auto"
|
||||
icon={<FiSave className="w-5 h-5" />}
|
||||
active
|
||||
border
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<FormFooter
|
||||
dirty={formState.isDirty}
|
||||
saveAction={onSubmit}
|
||||
clearAction={reset}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
<Cover enabled={debug} content={<JSONPretty data={channels} />} />
|
||||
<div className="w-full p-4 space-y-2 md:p-10">
|
||||
|
||||
2
todo.txt
2
todo.txt
@@ -9,6 +9,8 @@ form prefix should be located in the input (absolute?)
|
||||
form suffix should focus input
|
||||
reset store on new connection
|
||||
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
|
||||
- fix entering device-reconnecting state and not re-connecting despite packets being received
|
||||
Reference in New Issue
Block a user