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

View File

@@ -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
View File

@@ -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:

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>

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>

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" />}
/>

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>
);
};

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>
);
};

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>
);
};

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>
);
};

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>

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" />

View File

@@ -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));

View File

@@ -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;

View File

@@ -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">

View File

@@ -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