Browse Source

WIP 1.3

pull/22/head
Sacha Weatherstone 4 years ago
parent
commit
806e70b3cf
No known key found for this signature in database GPG Key ID: 7AB2D7E206124B31
  1. 25
      package.json
  2. 1524
      pnpm-lock.yaml
  3. 47
      src/components/Connection.tsx
  4. 20
      src/components/layout/Sidebar/Settings/GPS.tsx
  5. 4
      src/components/layout/Sidebar/Settings/Index.tsx
  6. 6
      src/components/layout/Sidebar/Settings/LoRa.tsx
  7. 11
      src/components/layout/Sidebar/Settings/Power.tsx
  8. 2
      src/components/layout/Sidebar/Settings/User.tsx
  9. 16
      src/components/layout/Sidebar/Settings/modules/Serial.tsx
  10. 2
      src/components/layout/Sidebar/Settings/modules/Telemetry.tsx
  11. 60
      src/components/menu/BottomNav.tsx
  12. 100
      src/core/slices/meshtasticSlice.ts
  13. 11
      src/pages/Extensions/Debug.tsx
  14. 4
      src/pages/Extensions/Info.tsx
  15. 20
      src/pages/Map/index.tsx
  16. 4
      src/pages/Messages/ChannelChat.tsx
  17. 12
      src/pages/Messages/DmChat.tsx
  18. 6
      src/pages/Messages/Message.tsx
  19. 8
      src/pages/Messages/index.tsx
  20. 36
      src/pages/Nodes/NodeCard.tsx
  21. 40
      src/pages/Nodes/index.tsx
  22. 2
      types/static.d.ts

25
package.json

@ -22,18 +22,18 @@
"dependencies": { "dependencies": {
"@emeraldpay/hashicon-react": "^0.5.2", "@emeraldpay/hashicon-react": "^0.5.2",
"@meshtastic/eslint-config": "^1.0.7", "@meshtastic/eslint-config": "^1.0.7",
"@meshtastic/meshtasticjs": "^0.6.51", "@meshtastic/meshtasticjs": "^0.6.54",
"@reduxjs/toolkit": "^1.8.0", "@reduxjs/toolkit": "^1.8.0",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"framer-motion": "^6.2.8", "framer-motion": "^6.2.8",
"mapbox-gl": "^2.7.0", "mapbox-gl": "^2.7.1",
"prettier": "^2.5.1", "prettier": "^2.6.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-flow-renderer": "^10.0.0-next.51", "react-flow-renderer": "^10.0.6",
"react-hook-form": "^7.27.1", "react-hook-form": "^7.28.1",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0", "react-json-pretty": "^2.2.0",
"react-multi-select-component": "^4.2.3", "react-multi-select-component": "^4.2.3",
@ -48,23 +48,24 @@
}, },
"devDependencies": { "devDependencies": {
"@hookform/devtools": "^4.0.2", "@hookform/devtools": "^4.0.2",
"@types/chrome": "^0.0.180",
"@types/mapbox-gl": "^2.6.3", "@types/mapbox-gl": "^2.6.3",
"@types/react": "^17.0.39", "@types/react": "^17.0.43",
"@types/react-dom": "^17.0.13", "@types/react-dom": "^17.0.14",
"@types/w3c-web-serial": "^1.0.2", "@types/w3c-web-serial": "^1.0.2",
"@types/web-bluetooth": "^0.0.12", "@types/web-bluetooth": "^0.0.14",
"@vitejs/plugin-react": "^1.2.0", "@vitejs/plugin-react": "^1.2.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.4",
"gzipper": "^7.0.0", "gzipper": "^7.0.0",
"postcss": "^8.4.8", "postcss": "^8.4.12",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"tailwindcss": "^3.0.23", "tailwindcss": "^3.0.23",
"tar": "^6.1.11", "tar": "^6.1.11",
"typescript": "^4.6.2", "typescript": "^4.6.3",
"unimported": "^1.19.1", "unimported": "^1.19.1",
"vite": "^2.8.6", "vite": "^2.8.6",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-pwa": "^0.11.13", "vite-plugin-pwa": "^0.11.13",
"workbox-window": "^6.5.1" "workbox-window": "^6.5.2"
} }
} }

1524
pnpm-lock.yaml

File diff suppressed because it is too large

47
src/components/Connection.tsx

@ -24,6 +24,7 @@ export const Connection = (): JSX.Element => {
const meshtasticState = useAppSelector((state) => state.meshtastic); const meshtasticState = useAppSelector((state) => state.meshtastic);
const appState = useAppSelector((state) => state.app); const appState = useAppSelector((state) => state.app);
const chromiunm = !!window.chrome;
useEffect(() => { useEffect(() => {
if (!import.meta.env.VITE_PUBLIC_HOSTED) { if (!import.meta.env.VITE_PUBLIC_HOSTED) {
@ -79,22 +80,36 @@ export const Connection = (): JSX.Element => {
} }
/> />
)} )}
{appState.connType === connType.BLE && (
<BLE {appState.connType === connType.BLE &&
connecting={ (chromiunm ? (
meshtasticState.deviceStatus === <BLE
Types.DeviceStatusEnum.DEVICE_CONNECTED connecting={
} meshtasticState.deviceStatus ===
/> Types.DeviceStatusEnum.DEVICE_CONNECTED
)} }
{appState.connType === connType.SERIAL && ( />
<Serial ) : (
connecting={ <div className="rounded-md border border-red-500 bg-red-500 bg-opacity-10 p-8 dark:text-white">
meshtasticState.deviceStatus === <p>Unsupported.</p>
Types.DeviceStatusEnum.DEVICE_CONNECTED <p>Please use a Chromium based browser.</p>
} </div>
/> ))}
)}
{appState.connType === connType.SERIAL &&
(chromiunm ? (
<Serial
connecting={
meshtasticState.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED
}
/>
) : (
<div className="rounded-md border border-red-500 bg-red-500 bg-opacity-10 p-8 dark:text-white">
<p>Unsupported.</p>
<p>Please use a Chromium based browser.</p>
</div>
))}
</div> </div>
</div> </div>
<div className="md:w-1/2"> <div className="md:w-1/2">

20
src/components/layout/Sidebar/Settings/GPS.tsx

@ -8,7 +8,6 @@ import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form'; import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input'; import { Input } from '@components/generic/form/Input';
import { Label } from '@components/generic/form/Label'; import { Label } from '@components/generic/form/Label';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection'; import { connection } from '@core/connection';
import { bitwiseDecode, bitwiseEncode } from '@core/utils/bitwise'; import { bitwiseDecode, bitwiseEncode } from '@core/utils/bitwise';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
@ -25,7 +24,7 @@ export const GPS = (): JSX.Element => {
...preferences, ...preferences,
positionBroadcastSecs: positionBroadcastSecs:
preferences.positionBroadcastSecs === 0 preferences.positionBroadcastSecs === 0
? preferences.isRouter ? preferences.role === Protobuf.Role.Router
? 43200 ? 43200
: 900 : 900
: preferences.positionBroadcastSecs, : preferences.positionBroadcastSecs,
@ -54,20 +53,15 @@ export const GPS = (): JSX.Element => {
{...register('positionBroadcastSecs', { valueAsNumber: true })} {...register('positionBroadcastSecs', { valueAsNumber: true })}
/> />
<Checkbox <Checkbox
label="Use Smart Position" label="Disable Smart Position"
{...register('positionBroadcastSmart')} {...register('positionBroadcastSmartDisabled')}
/> />
<Checkbox label="Use Fixed Position" {...register('fixedPosition')} /> <Checkbox label="Use Fixed Position" {...register('fixedPosition')} />
<Select <Checkbox
label="Location Sharing" label="Disable Location Sharing"
optionsEnum={Protobuf.LocationSharing} {...register('locationShareDisabled')}
{...register('locationShare', { valueAsNumber: true })}
/>
<Select
label="GPS Mode"
optionsEnum={Protobuf.GpsOperation}
{...register('gpsOperation', { valueAsNumber: true })}
/> />
<Checkbox label="Disable GPS" {...register('gpsDisabled')} />
<Input <Input
label="GPS Update Interval" label="GPS Update Interval"
type="number" type="number"

4
src/components/layout/Sidebar/Settings/Index.tsx

@ -51,7 +51,7 @@ export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
const { const {
rangeTestModuleEnabled, rangeTestModuleEnabled,
extNotificationModuleEnabled, extNotificationModuleEnabled,
serialmoduleEnabled, serialModuleEnabled,
storeForwardModuleEnabled, storeForwardModuleEnabled,
mqttDisabled, mqttDisabled,
cannedMessageModuleEnabled, cannedMessageModuleEnabled,
@ -132,7 +132,7 @@ export const Settings = ({ open, setOpen }: SettingsProps): JSX.Element => {
<CollapsibleSection <CollapsibleSection
icon={<FiAlignLeft />} icon={<FiAlignLeft />}
title="Serial" title="Serial"
status={serialmoduleEnabled} status={serialModuleEnabled}
> >
<SerialSettingsPanel /> <SerialSettingsPanel />
</CollapsibleSection> </CollapsibleSection>

6
src/components/layout/Sidebar/Settings/LoRa.tsx

@ -42,7 +42,11 @@ export const LoRa = (): JSX.Element => {
{...register('hopLimit', { valueAsNumber: true })} {...register('hopLimit', { valueAsNumber: true })}
/> />
<Checkbox label="Transmit Disabled" {...register('isLoraTxDisabled')} /> <Checkbox label="Transmit Disabled" {...register('isLoraTxDisabled')} />
<Checkbox label="Router Mode" {...register('isRouter')} /> <Select
label="Operating Role"
optionsEnum={Protobuf.Role}
{...register('role')}
/>
<Input <Input
label="Send Owner Interval" label="Send Owner Interval"
type="number" type="number"

11
src/components/layout/Sidebar/Settings/Power.tsx

@ -20,7 +20,10 @@ export const Power = (): JSX.Element => {
useForm<Protobuf.RadioConfig_UserPreferences>({ useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: { defaultValues: {
...preferences, ...preferences,
isLowPower: preferences.isRouter ? true : preferences.isLowPower, isLowPower:
preferences.role === Protobuf.Role.Router
? true
: preferences.isLowPower,
}, },
}); });
@ -45,9 +48,11 @@ export const Power = (): JSX.Element => {
/> />
<Checkbox <Checkbox
label="Powered by low power source (solar)" label="Powered by low power source (solar)"
disabled={preferences.isRouter} disabled={preferences.role === Protobuf.Role.Router}
validationMessage={ validationMessage={
preferences.isRouter ? 'Enabled by default in router mode' : '' preferences.role === Protobuf.Role.Router
? 'Enabled by default in router mode'
: ''
} }
{...register('isLowPower')} {...register('isLowPower')}
/> />

2
src/components/layout/Sidebar/Settings/User.tsx

@ -18,7 +18,7 @@ export const User = (): JSX.Element => {
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
).myNodeNum; ).myNodeNum;
const node = useAppSelector((state) => state.meshtastic.nodes).find( const node = useAppSelector((state) => state.meshtastic.nodes).find(
(node) => node.number === myNodeNum, (node) => node.num === myNodeNum,
); );
const { register, handleSubmit, formState, reset } = useForm<{ const { register, handleSubmit, formState, reset } = useForm<{
longName: string; longName: string;

16
src/components/layout/Sidebar/Settings/modules/Serial.tsx

@ -37,24 +37,24 @@ export const SerialSettingsPanel = (): JSX.Element => {
const moduleEnabled = useWatch({ const moduleEnabled = useWatch({
control, control,
name: 'serialmoduleEnabled', name: 'serialModuleEnabled',
defaultValue: false, defaultValue: false,
}); });
return ( return (
<Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}> <Form loading={loading} dirty={!formState.isDirty} submit={onSubmit}>
<Checkbox label="Module Enabled" {...register('serialmoduleEnabled')} /> <Checkbox label="Module Enabled" {...register('serialModuleEnabled')} />
<Checkbox <Checkbox
label="Echo" label="Echo"
disabled={!moduleEnabled} disabled={!moduleEnabled}
{...register('serialmoduleEcho')} {...register('serialModuleEcho')}
/> />
<Input <Input
type="number" type="number"
label="RX" label="RX"
disabled={!moduleEnabled} disabled={!moduleEnabled}
{...register('serialmoduleRxd', { {...register('serialModuleRxd', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -62,7 +62,7 @@ export const SerialSettingsPanel = (): JSX.Element => {
type="number" type="number"
label="TX Pin" label="TX Pin"
disabled={!moduleEnabled} disabled={!moduleEnabled}
{...register('serialmoduleTxd', { {...register('serialModuleTxd', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -70,7 +70,7 @@ export const SerialSettingsPanel = (): JSX.Element => {
type="number" type="number"
label="Baud Rate" label="Baud Rate"
disabled={!moduleEnabled} disabled={!moduleEnabled}
{...register('serialmoduleBaud', { {...register('serialModuleBaud', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -78,7 +78,7 @@ export const SerialSettingsPanel = (): JSX.Element => {
type="number" type="number"
label="Timeout" label="Timeout"
disabled={!moduleEnabled} disabled={!moduleEnabled}
{...register('serialmoduleTimeout', { {...register('serialModuleTimeout', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />
@ -86,7 +86,7 @@ export const SerialSettingsPanel = (): JSX.Element => {
type="number" type="number"
label="Mode" label="Mode"
disabled={!moduleEnabled} disabled={!moduleEnabled}
{...register('serialmoduleMode', { {...register('serialModuleMode', {
valueAsNumber: true, valueAsNumber: true,
})} })}
/> />

2
src/components/layout/Sidebar/Settings/modules/Telemetry.tsx

@ -68,7 +68,7 @@ export const Telemetry = (): JSX.Element => {
/> />
<Checkbox <Checkbox
label="Display Farenheit" label="Display Farenheit"
{...register('telemetryModuleDisplayFarenheit')} {...register('telemetryModuleDisplayFahrenheit')}
/> />
<Select <Select
label="Sensor Type" label="Sensor Type"

60
src/components/menu/BottomNav.tsx

@ -5,6 +5,7 @@ import {
FiBluetooth, FiBluetooth,
FiCpu, FiCpu,
FiGitBranch, FiGitBranch,
FiHexagon,
FiMenu, FiMenu,
FiMoon, FiMoon,
FiSun, FiSun,
@ -12,6 +13,7 @@ import {
FiX, FiX,
} from 'react-icons/fi'; } from 'react-icons/fi';
import { MdUpgrade } from 'react-icons/md'; import { MdUpgrade } from 'react-icons/md';
import { AiOutlineHeart, AiFillHeart } from 'react-icons/ai';
import { import {
RiArrowDownLine, RiArrowDownLine,
RiArrowUpDownLine, RiArrowUpDownLine,
@ -29,6 +31,13 @@ import {
import { useAppDispatch } from '@hooks/useAppDispatch'; import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf, Types } from '@meshtastic/meshtasticjs'; import { Protobuf, Types } from '@meshtastic/meshtasticjs';
import {
IoBatteryChargingOutline,
IoBatteryDeadOutline,
IoBatteryFullOutline,
IoBatteryHalfOutline,
} from 'react-icons/io5';
import { FaTrafficLight } from 'react-icons/fa';
export const BottomNav = (): JSX.Element => { export const BottomNav = (): JSX.Element => {
const [showVersionInfo, setShowVersionInfo] = useState(false); const [showVersionInfo, setShowVersionInfo] = useState(false);
@ -38,6 +47,8 @@ export const BottomNav = (): JSX.Element => {
const primaryChannelSettings = useAppSelector( const primaryChannelSettings = useAppSelector(
(state) => state.meshtastic.radio.channels, (state) => state.meshtastic.radio.channels,
).find((channel) => channel.role === Protobuf.Channel_Role.PRIMARY)?.settings; ).find((channel) => channel.role === Protobuf.Channel_Role.PRIMARY)?.settings;
const telemetry =
meshtasticState.nodes[meshtasticState.radio.hardware.myNodeNum]?.telemetry;
return ( return (
<div className="z-20 flex justify-between divide-x divide-gray-400 border-t border-gray-400 bg-white dark:divide-gray-600 dark:border-gray-600 dark:bg-secondaryDark"> <div className="z-20 flex justify-between divide-x divide-gray-400 border-t border-gray-400 bg-white dark:divide-gray-600 dark:border-gray-600 dark:bg-secondaryDark">
@ -78,10 +89,57 @@ export const BottomNav = (): JSX.Element => {
)} )}
<div className="truncate text-xs font-medium"> <div className="truncate text-xs font-medium">
{meshtasticState.nodes.find( {meshtasticState.nodes.find(
(node) => node.number === meshtasticState.radio.hardware.myNodeNum, (node) => node.num === meshtasticState.radio.hardware.myNodeNum,
)?.user?.longName ?? 'Disconnected'} )?.user?.longName ?? 'Disconnected'}
</div> </div>
</BottomNavItem> </BottomNavItem>
<BottomNavItem tooltip="Router Heartbeat">
{telemetry?.routerHeartbeat ? (
<AiFillHeart className="h-4" />
) : (
<AiOutlineHeart className="h-4" />
)}
</BottomNavItem>
<BottomNavItem tooltip="Battery Level">
{!telemetry?.batteryLevel ? (
<IoBatteryDeadOutline className="h-4" />
) : telemetry?.batteryLevel > 50 ? (
<IoBatteryFullOutline className="h-4" />
) : telemetry?.batteryLevel > 0 ? (
<IoBatteryFullOutline className="h-4" />
) : (
<IoBatteryChargingOutline className="h-4" />
)}
<div className="truncate text-xs font-medium">
{telemetry?.batteryLevel
? `${telemetry?.batteryLevel}% - ${telemetry?.voltage}v`
: 'No Battery'}
</div>
</BottomNavItem>
<BottomNavItem tooltip="Network Utilization">
<div className="m-auto h-3 w-3 rounded-full bg-primary" />
<div className="truncate text-xs font-medium">
{`${telemetry?.airUtilTx ?? 0}% - Air`} |
</div>
<div
className={`m-auto h-3 w-3 rounded-full ${
!telemetry?.channelUtilization
? 'bg-primary'
: telemetry?.channelUtilization > 50
? 'bg-red-400'
: telemetry?.channelUtilization > 24
? 'bg-yellow-400'
: 'bg-primary'
}`}
/>
<div className="truncate text-xs font-medium">
{`${telemetry?.channelUtilization ?? 0}% - Ch`}
</div>
</BottomNavItem>
<BottomNavItem tooltip="MQTT Status"> <BottomNavItem tooltip="MQTT Status">
{primaryChannelSettings?.uplinkEnabled && {primaryChannelSettings?.uplinkEnabled &&

100
src/core/slices/meshtasticSlice.ts

@ -33,16 +33,6 @@ interface Route {
//speed stats? //speed stats?
} }
export interface Node {
number: number;
lastHeard: Date;
snr: number[];
positions: Protobuf.Position[];
currentPosition?: CurrentPosition;
user?: Protobuf.User;
routes: Route[];
}
export interface Radio { export interface Radio {
channels: Protobuf.Channel[]; channels: Protobuf.Channel[];
preferences: Protobuf.RadioConfig_UserPreferences; preferences: Protobuf.RadioConfig_UserPreferences;
@ -53,7 +43,7 @@ interface MeshtasticState {
deviceStatus: Types.DeviceStatusEnum; deviceStatus: Types.DeviceStatusEnum;
lastMeshInterraction: number; lastMeshInterraction: number;
ready: boolean; ready: boolean;
nodes: Node[]; nodes: Protobuf.NodeInfo[];
radio: Radio; radio: Radio;
chats: ChatEntries; chats: ChatEntries;
logs: Types.LogEventPacket[]; logs: Types.LogEventPacket[];
@ -97,73 +87,46 @@ export const meshtasticSlice = createSlice({
}, },
addUser: (state, action: PayloadAction<Types.UserPacket>) => { addUser: (state, action: PayloadAction<Types.UserPacket>) => {
const node = state.nodes.find( const node = state.nodes.find(
(node) => node.number === action.payload.packet.from, (node) => node.num === action.payload.packet.from,
); );
if (node) { if (node) {
node.user = action.payload.data; node.user = action.payload.data;
if (action.payload.packet.rxTime) { if (action.payload.packet.rxTime) {
node.lastHeard = new Date(action.payload.packet.rxTime * 1000); node.lastHeard = new Date(
action.payload.packet.rxTime * 1000,
).getTime();
} }
} else { } else {
state.nodes.push({ state.nodes.push({
number: action.payload.packet.from, num: action.payload.packet.from,
lastHeard: new Date(), snr: action.payload.packet.rxSnr,
snr: [action.payload.packet.rxSnr], lastHeard: new Date().getTime(),
user: action.payload.data, ...action.payload.packet,
positions: [],
routes: [],
}); });
} }
}, },
addPosition: (state, action: PayloadAction<Types.PositionPacket>) => { addPosition: (state, action: PayloadAction<Types.PositionPacket>) => {
const node = state.nodes.find( const node = state.nodes.find(
(node) => node.number === action.payload.packet.from, (node) => node.num === action.payload.packet.from,
); );
if (node) { if (node) {
node.positions.push(action.payload.data); node.position = action.payload.data;
if (
action.payload.data.latitudeI ||
action.payload.data.longitudeI ||
action.payload.data.altitude
) {
node.currentPosition = {
latitudeI:
action.payload.data.latitudeI ?? node.currentPosition?.latitudeI,
longitudeI:
action.payload.data.longitudeI ??
node.currentPosition?.longitudeI,
altitude:
action.payload.data.altitude ?? node.currentPosition?.altitude,
posTimestamp: action.payload.data.posTimestamp,
satsInView:
action.payload.data.satsInView ??
node.currentPosition?.satsInView,
};
}
if (action.payload.packet.rxTime) { if (action.payload.packet.rxTime) {
node.lastHeard = new Date(action.payload.packet.rxTime * 1000); node.lastHeard = new Date(
action.payload.packet.rxTime * 1000,
).getTime();
} }
} }
}, },
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => { addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
const node = state.nodes.find( const node = state.nodes.find((node) => node.num === action.payload.num);
(node) => node.number === action.payload.num,
);
if (node) { if (node) {
node.lastHeard = new Date(action.payload.lastHeard * 1000); node.lastHeard = new Date(action.payload.lastHeard * 1000).getTime();
node.snr.push(action.payload.snr); node.snr = action.payload.snr;
} else { } else {
state.nodes.push({ state.nodes.push(action.payload);
number: action.payload.num,
lastHeard: new Date(action.payload.lastHeard * 1000),
snr: [action.payload.snr],
positions: [],
routes: [],
});
} }
}, },
addChannel: (state, action: PayloadAction<Protobuf.Channel>) => { addChannel: (state, action: PayloadAction<Protobuf.Channel>) => {
@ -182,17 +145,16 @@ export const meshtasticSlice = createSlice({
} }
}, },
addRoute: (state, action: PayloadAction<Route>) => { addRoute: (state, action: PayloadAction<Route>) => {
const node = state.nodes.find( // const node = state.nodes.find(
(node) => node.number === action.payload.from, // (node) => node.num === action.payload.from,
); // );
const exists = node?.routes.findIndex( // const exists = node?.routes.findIndex(
(route) => // (route) =>
route.from === action.payload.from && route.to === action.payload.to, // route.from === action.payload.from && route.to === action.payload.to,
); // );
// if (exists === -1) {
if (exists === -1) { // node?.routes.push(action.payload);
node?.routes.push(action.payload); // }
}
}, },
setPreferences: ( setPreferences: (
state, state,
@ -243,11 +205,9 @@ export const meshtasticSlice = createSlice({
state, state,
action: PayloadAction<{ id: number; time: Date }>, action: PayloadAction<{ id: number; time: Date }>,
) => { ) => {
const node = state.nodes.find( const node = state.nodes.find((node) => node.num === action.payload.id);
(node) => node.number === action.payload.id,
);
if (node) { if (node) {
node.lastHeard = action.payload.time; node.lastHeard = action.payload.time.getTime();
} }
}, },
addChat: (state, action: PayloadAction<number>) => { addChat: (state, action: PayloadAction<number>) => {

11
src/pages/Extensions/Debug.tsx

@ -10,9 +10,7 @@ export const Debug = (): JSX.Element => {
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
); );
const node = useAppSelector((state) => const node = useAppSelector((state) =>
state.meshtastic.nodes.find( state.meshtastic.nodes.find((node) => node.num === hardwareInfo.myNodeNum),
(node) => node.number === hardwareInfo.myNodeNum,
),
); );
return ( return (
@ -33,6 +31,13 @@ export const Debug = (): JSX.Element => {
> >
Get Preferences Get Preferences
</Button> </Button>
<Button
onClick={async (): Promise<void> => {
await connection.getAllChannels();
}}
>
Get All Channels
</Button>
</div> </div>
</Card> </Card>
</div> </div>

4
src/pages/Extensions/Info.tsx

@ -14,9 +14,7 @@ export const Info = (): JSX.Element => {
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
); );
const node = useAppSelector((state) => const node = useAppSelector((state) =>
state.meshtastic.nodes.find( state.meshtastic.nodes.find((node) => node.num === hardwareInfo.myNodeNum),
(node) => node.number === hardwareInfo.myNodeNum,
),
); );
return ( return (

20
src/pages/Map/index.tsx

@ -7,14 +7,14 @@ import { RiRoadMapLine } from 'react-icons/ri';
import { Layout } from '@components/layout'; import { Layout } from '@components/layout';
import { MapboxProvider } from '@components/MapBox/MapboxProvider'; import { MapboxProvider } from '@components/MapBox/MapboxProvider';
import type { Node } from '@core/slices/meshtasticSlice';
import { useAppSelector } from '@hooks/useAppSelector'; import { useAppSelector } from '@hooks/useAppSelector';
import { MapContainer } from '@pages/Map/MapContainer'; import { MapContainer } from '@pages/Map/MapContainer';
import { Marker } from '@pages/Map/Marker'; import { Marker } from '@pages/Map/Marker';
import { NodeCard } from '@pages/Nodes/NodeCard'; import { NodeCard } from '@pages/Nodes/NodeCard';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const Map = (): JSX.Element => { export const Map = (): JSX.Element => {
const [selectedNode, setSelectedNode] = useState<Node>(); const [selectedNode, setSelectedNode] = useState<Protobuf.NodeInfo>();
const nodes = useAppSelector((state) => state.meshtastic.nodes); const nodes = useAppSelector((state) => state.meshtastic.nodes);
const myNodeNum = useAppSelector( const myNodeNum = useAppSelector(
@ -25,13 +25,13 @@ export const Map = (): JSX.Element => {
<MapboxProvider> <MapboxProvider>
{nodes.map((node) => { {nodes.map((node) => {
return ( return (
node.currentPosition && ( node.position && (
<Marker <Marker
key={node.number} key={node.num}
center={ center={
new mapboxgl.LngLat( new mapboxgl.LngLat(
node.currentPosition.longitudeI / 1e7, node.position.longitudeI / 1e7,
node.currentPosition.latitudeI / 1e7, node.position.latitudeI / 1e7,
) )
} }
> >
@ -40,7 +40,7 @@ export const Map = (): JSX.Element => {
setSelectedNode(node); setSelectedNode(node);
}} }}
className={`z-50 rounded-full border-2 bg-opacity-30 ${ className={`z-50 rounded-full border-2 bg-opacity-30 ${
node.number === selectedNode?.number node.num === selectedNode?.num
? 'border-green-500 bg-green-500' ? 'border-green-500 bg-green-500'
: 'border-blue-500 bg-blue-500' : 'border-blue-500 bg-blue-500'
}`} }`}
@ -66,10 +66,10 @@ export const Map = (): JSX.Element => {
{nodes.map((node) => ( {nodes.map((node) => (
<NodeCard <NodeCard
key={node.number} key={node.num}
node={node} node={node}
isMyNode={node.number === myNodeNum} isMyNode={node.num === myNodeNum}
selected={selectedNode?.number === node.number} selected={selectedNode?.num === node.num}
setSelected={(): void => { setSelected={(): void => {
setSelectedNode(node); setSelectedNode(node);
}} }}

4
src/pages/Messages/ChannelChat.tsx

@ -27,7 +27,7 @@ export const ChannelChat = ({
(state) => state.meshtastic.radio.hardware, (state) => state.meshtastic.radio.hardware,
).myNodeNum; ).myNodeNum;
const nodes = useAppSelector((state) => state.meshtastic.nodes).filter( const nodes = useAppSelector((state) => state.meshtastic.nodes).filter(
(node) => node.number !== myNodeNum, (node) => node.num !== myNodeNum,
); );
const chats = useAppSelector((state) => state.meshtastic.chats); const chats = useAppSelector((state) => state.meshtastic.chats);
const channels = useAppSelector( const channels = useAppSelector(
@ -80,7 +80,7 @@ export const ChannelChat = ({
<Tooltip <Tooltip
key={nodeId} key={nodeId}
content={ content={
nodes.find((node) => node.number === nodeId)?.user nodes.find((node) => node.num === nodeId)?.user
?.longName ?? 'UNK' ?.longName ?? 'UNK'
} }
> >

12
src/pages/Messages/DmChat.tsx

@ -4,11 +4,11 @@ import { FiSettings } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton'; import { IconButton } from '@components/generic/button/IconButton';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem'; import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import type { Node } from '@core/slices/meshtasticSlice';
import { Hashicon } from '@emeraldpay/hashicon-react'; import { Hashicon } from '@emeraldpay/hashicon-react';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface DmChatProps { export interface DmChatProps {
node: Node; node: Protobuf.NodeInfo;
selectedIndex: number; selectedIndex: number;
setSelectedIndex: (index: number) => void; setSelectedIndex: (index: number) => void;
} }
@ -20,16 +20,16 @@ export const DmChat = ({
}: DmChatProps): JSX.Element => { }: DmChatProps): JSX.Element => {
return ( return (
<SidebarItem <SidebarItem
key={node.number} key={node.num}
selected={node.number === selectedIndex} selected={node.num === selectedIndex}
setSelected={(): void => { setSelected={(): void => {
setSelectedIndex(node.number); setSelectedIndex(node.num);
}} }}
actions={<IconButton nested icon={<FiSettings />} />} actions={<IconButton nested icon={<FiSettings />} />}
> >
<div className="flex dark:text-white"> <div className="flex dark:text-white">
<div className="m-auto"> <div className="m-auto">
<Hashicon value={node.number.toString()} size={32} /> <Hashicon value={node.num.toString()} size={32} />
</div> </div>
</div> </div>
<div className="my-auto mr-auto font-semibold dark:text-white"> <div className="my-auto mr-auto font-semibold dark:text-white">

6
src/pages/Messages/Message.tsx

@ -3,15 +3,15 @@ import type React from 'react';
import { FiClock } from 'react-icons/fi'; import { FiClock } from 'react-icons/fi';
import { Tooltip } from '@components/generic/Tooltip'; import { Tooltip } from '@components/generic/Tooltip';
import type { Node } from '@core/slices/meshtasticSlice';
import { Hashicon } from '@emeraldpay/hashicon-react'; import { Hashicon } from '@emeraldpay/hashicon-react';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface MessageProps { export interface MessageProps {
lastMsgSameUser: boolean; lastMsgSameUser: boolean;
message: string; message: string;
ack: boolean; ack: boolean;
rxTime: Date; rxTime: Date;
sender?: Node; sender?: Protobuf.NodeInfo;
} }
export const Message = ({ export const Message = ({
@ -59,7 +59,7 @@ export const Message = ({
) : ( ) : (
<div className="mx-6 flex gap-2"> <div className="mx-6 flex gap-2">
<div className="my-auto w-8"> <div className="my-auto w-8">
<Hashicon value={(sender?.number ?? 0).toString()} size={32} /> <Hashicon value={(sender?.num ?? 0).toString()} size={32} />
</div> </div>
<div> <div>
<div className="flex gap-2"> <div className="flex gap-2">

8
src/pages/Messages/index.tsx

@ -52,10 +52,10 @@ export const Messages = (): JSX.Element => {
<div className="mx-2 border-b border-gray-400 dark:border-gray-600" /> <div className="mx-2 border-b border-gray-400 dark:border-gray-600" />
)} )}
{nodes {nodes
.filter((node) => node.number !== myNodeNum) .filter((node) => node.num !== myNodeNum)
.map((node) => ( .map((node) => (
<DmChat <DmChat
key={node.number} key={node.num}
node={node} node={node}
selectedIndex={selectedChatIndex} selectedIndex={selectedChatIndex}
setSelectedIndex={setSelectedChatIndex} setSelectedIndex={setSelectedChatIndex}
@ -81,7 +81,7 @@ export const Messages = (): JSX.Element => {
</span> </span>
) : ( ) : (
<span className="text-gray-500 dark:text-gray-400"> <span className="text-gray-500 dark:text-gray-400">
{nodes.find((n) => n.number === selectedChatIndex)?.user {nodes.find((node) => node.num === selectedChatIndex)?.user
?.longName ?? 'Unknown'} ?.longName ?? 'Unknown'}
</span> </span>
)} )}
@ -106,7 +106,7 @@ export const Messages = (): JSX.Element => {
.packet.from === message.message.packet.from .packet.from === message.message.packet.from
} }
sender={nodes.find( sender={nodes.find(
(node) => node.number === message.message.packet.from, (node) => node.num === message.message.packet.from,
)} )}
/> />
))} ))}

36
src/pages/Nodes/NodeCard.tsx

@ -21,14 +21,14 @@ import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
import { Tooltip } from '@components/generic/Tooltip'; import { Tooltip } from '@components/generic/Tooltip';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem'; import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { CopyButton } from '@components/menu/buttons/CopyButton'; import { CopyButton } from '@components/menu/buttons/CopyButton';
import type { Node } from '@core/slices/meshtasticSlice';
import { Hashicon } from '@emeraldpay/hashicon-react'; import { Hashicon } from '@emeraldpay/hashicon-react';
import { useMapbox } from '@hooks/useMapbox'; import { useMapbox } from '@hooks/useMapbox';
import type { Protobuf } from '@meshtastic/meshtasticjs';
type PositionConfidence = 'high' | 'low' | 'none'; type PositionConfidence = 'high' | 'low' | 'none';
export interface NodeCardProps { export interface NodeCardProps {
node: Node; node: Protobuf.NodeInfo;
isMyNode: boolean; isMyNode: boolean;
selected: boolean; selected: boolean;
setSelected: () => void; setSelected: () => void;
@ -47,14 +47,14 @@ export const NodeCard = ({
useEffect(() => { useEffect(() => {
setPositionConfidence( setPositionConfidence(
node.currentPosition node.position
? new Date(node.currentPosition.posTimestamp * 1000) > ? new Date(node.position.posTimestamp * 1000) >
new Date(new Date().getTime() - 1000 * 60 * 30) new Date(new Date().getTime() - 1000 * 60 * 30)
? 'high' ? 'high'
: 'low' : 'low'
: 'none', : 'none',
); );
}, [node.currentPosition]); }, [node.position]);
return ( return (
<> <>
<SidebarItem <SidebarItem
@ -69,11 +69,11 @@ export const NodeCard = ({
onClick={(e): void => { onClick={(e): void => {
e.stopPropagation(); e.stopPropagation();
setSelected(); setSelected();
if (PositionConfidence !== 'none' && node.currentPosition) { if (PositionConfidence !== 'none' && node.position) {
map?.flyTo({ map?.flyTo({
center: new LngLat( center: new LngLat(
node.currentPosition.longitudeI / 1e7, node.position.longitudeI / 1e7,
node.currentPosition.latitudeI / 1e7, node.position.latitudeI / 1e7,
), ),
zoom: 16, zoom: 16,
}); });
@ -113,12 +113,12 @@ export const NodeCard = ({
</m.div> </m.div>
</Tooltip> </Tooltip>
)} )}
<Hashicon value={node.number.toString()} size={32} /> <Hashicon value={node.num.toString()} size={32} />
</div> </div>
</div> </div>
<div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400"> <div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400">
{node.lastHeard.getTime() {node.lastHeard
? node.lastHeard.toLocaleTimeString(undefined, { ? new Date(node.lastHeard).toLocaleTimeString(undefined, {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}) })
@ -136,7 +136,7 @@ export const NodeCard = ({
<CollapsibleSection title="User" icon={<FiUser />}> <CollapsibleSection title="User" icon={<FiUser />}>
<div className="flex p-2"> <div className="flex p-2">
<div className="m-auto flex flex-col gap-2"> <div className="m-auto flex flex-col gap-2">
<Hashicon value={node.number.toString()} size={180} /> <Hashicon value={node.num.toString()} size={180} />
<div className="text-center text-lg font-medium dark:text-white"> <div className="text-center text-lg font-medium dark:text-white">
{node?.user?.longName || 'Unknown'} {node?.user?.longName || 'Unknown'}
</div> </div>
@ -146,18 +146,18 @@ export const NodeCard = ({
<CollapsibleSection title="Location" icon={<FiMapPin />}> <CollapsibleSection title="Location" icon={<FiMapPin />}>
<> <>
<div className="flex h-10 select-none justify-between rounded-md border border-gray-400 bg-transparent bg-gray-300 px-1 text-gray-500 dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400 "> <div className="flex h-10 select-none justify-between rounded-md border border-gray-400 bg-transparent bg-gray-300 px-1 text-gray-500 dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400 ">
{node.currentPosition ? ( {node.position ? (
<> <>
<div className="my-auto px-1"> <div className="my-auto px-1">
{(node.currentPosition.latitudeI / 1e7).toPrecision(6)} {(node.position.latitudeI / 1e7).toPrecision(6)}
,&nbsp; ,&nbsp;
{(node.currentPosition?.longitudeI / 1e7).toPrecision(6)} {(node.position?.longitudeI / 1e7).toPrecision(6)}
</div> </div>
<CopyButton <CopyButton
data={ data={
node.currentPosition node.position
? `${node.currentPosition.latitudeI / 1e7},${ ? `${node.position.latitudeI / 1e7},${
node.currentPosition.longitudeI / 1e7 node.position.longitudeI / 1e7
}` }`
: '' : ''
} }

40
src/pages/Nodes/index.tsx

@ -37,8 +37,8 @@ export const Nodes = (): JSX.Element => {
nodes.map((node, index) => { nodes.map((node, index) => {
tmpNodes.push({ tmpNodes.push({
id: node.number.toString(), id: node.num.toString(),
data: { label: node.user?.longName ?? `Unknown ${node.number}` }, data: { label: node.user?.longName ?? `Unknown ${node.num}` },
position: { x: index * 160 + 500, y: 100 + 500 }, position: { x: index * 160 + 500, y: 100 + 500 },
}); });
}); });
@ -50,7 +50,7 @@ export const Nodes = (): JSX.Element => {
const tmpEdges: Edge[] = []; const tmpEdges: Edge[] = [];
nodes.map((node, index) => { nodes.map((node, index) => {
if (node.number === myNodeNum) { if (node.num === myNodeNum) {
tmpEdges.push({ tmpEdges.push({
id: `e${1}-${myNodeNum}`, id: `e${1}-${myNodeNum}`,
source: '1', source: '1',
@ -63,15 +63,15 @@ export const Nodes = (): JSX.Element => {
}); });
} }
node.routes.map((route) => { // node.routes.map((route) => {
tmpEdges.push({ // tmpEdges.push({
id: `e${route.from}-${route.to}`, // id: `e${route.from}-${route.to}`,
source: node.number.toString(), // source: node.num.toString(),
target: route.to.toString(), // target: route.to.toString(),
type: 'smoothstep', // type: 'smoothstep',
animated: true, // animated: true,
}); // });
}); // });
}); });
setGraphEdges(tmpEdges); setGraphEdges(tmpEdges);
@ -85,17 +85,17 @@ export const Nodes = (): JSX.Element => {
<> <>
{nodes.map((node) => ( {nodes.map((node) => (
<SidebarItem <SidebarItem
key={node.number} key={node.num}
selected={node.number === selected} selected={node.num === selected}
setSelected={(): void => { setSelected={(): void => {
setSelected(node.number); setSelected(node.num);
}} }}
actions={ actions={
<IconButton <IconButton
nested nested
onClick={(e): void => { onClick={(e): void => {
e.stopPropagation(); e.stopPropagation();
setSelected(node.number); setSelected(node.num);
}} }}
icon={<FiSettings />} icon={<FiSettings />}
/> />
@ -103,7 +103,7 @@ export const Nodes = (): JSX.Element => {
> >
<div className="flex dark:text-white"> <div className="flex dark:text-white">
<div className="relative m-auto"> <div className="relative m-auto">
{node.number === myNodeNum && ( {node.num === myNodeNum && (
<Tooltip content="Your Node"> <Tooltip content="Your Node">
<m.div <m.div
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
@ -113,12 +113,12 @@ export const Nodes = (): JSX.Element => {
</m.div> </m.div>
</Tooltip> </Tooltip>
)} )}
<Hashicon value={node.number.toString()} size={32} /> <Hashicon value={node.num.toString()} size={32} />
</div> </div>
</div> </div>
<div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400"> <div className="my-auto mr-auto text-xs font-semibold dark:text-gray-400">
{node.lastHeard.getTime() {node.lastHeard
? node.lastHeard.toLocaleTimeString(undefined, { ? new Date(node.lastHeard).toLocaleTimeString(undefined, {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}) })

2
types/static.d.ts

@ -1,6 +1,8 @@
/* Use this file to declare any custom file extensions for importing */ /* Use this file to declare any custom file extensions for importing */
/* Use this folder to also add/extend a package d.ts file, if needed. */ /* Use this folder to also add/extend a package d.ts file, if needed. */
///<reference types="chrome"/>
/* CSS MODULES */ /* CSS MODULES */
declare module '*.module.css' { declare module '*.module.css' {
const classes: { [key: string]: string }; const classes: { [key: string]: string };

Loading…
Cancel
Save