Browse Source

WIP updates

pull/31/head
Sacha Weatherstone 4 years ago
parent
commit
ed9ee28745
No known key found for this signature in database GPG Key ID: 7AB2D7E206124B31
  1. 4
      package.json
  2. 42
      pnpm-lock.yaml
  3. 19
      src/components/PageComponents/Config/Display.tsx
  4. 94
      src/components/PageComponents/Config/WiFi copy.tsx
  5. 69
      src/components/PageComponents/Config/WiFi.tsx
  6. 2
      src/components/layout/Sidebar/index.tsx
  7. 72
      src/core/stores/deviceStore.ts
  8. 14
      src/core/subscriptions.ts
  9. 5
      src/pages/Info/index.tsx
  10. 33
      src/pages/Messages/LocationMessage.tsx
  11. 130
      src/pages/Messages/Message.tsx
  12. 5
      src/validation/config/display.ts
  13. 14
      src/validation/config/wifi.ts

4
package.json

@ -24,7 +24,7 @@
"@emeraldpay/hashicon-react": "^0.5.2", "@emeraldpay/hashicon-react": "^0.5.2",
"@hookform/resolvers": "^2.9.7", "@hookform/resolvers": "^2.9.7",
"@meshtastic/eslint-config": "^1.0.8", "@meshtastic/eslint-config": "^1.0.8",
"@meshtastic/meshtasticjs": "^0.6.84", "@meshtastic/meshtasticjs": "^0.6.87",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
@ -49,7 +49,7 @@
"@types/chrome": "^0.0.193", "@types/chrome": "^0.0.193",
"@types/geodesy": "^2.2.3", "@types/geodesy": "^2.2.3",
"@types/node": "^18.6.4", "@types/node": "^18.6.4",
"@types/react": "^18.0.15", "@types/react": "^18.0.16",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/w3c-web-serial": "^1.0.2", "@types/w3c-web-serial": "^1.0.2",
"@types/web-bluetooth": "^0.0.15", "@types/web-bluetooth": "^0.0.15",

42
pnpm-lock.yaml

@ -5,11 +5,11 @@ specifiers:
'@emeraldpay/hashicon-react': ^0.5.2 '@emeraldpay/hashicon-react': ^0.5.2
'@hookform/resolvers': ^2.9.7 '@hookform/resolvers': ^2.9.7
'@meshtastic/eslint-config': ^1.0.8 '@meshtastic/eslint-config': ^1.0.8
'@meshtastic/meshtasticjs': ^0.6.84 '@meshtastic/meshtasticjs': ^0.6.87
'@types/chrome': ^0.0.193 '@types/chrome': ^0.0.193
'@types/geodesy': ^2.2.3 '@types/geodesy': ^2.2.3
'@types/node': ^18.6.4 '@types/node': ^18.6.4
'@types/react': ^18.0.15 '@types/react': ^18.0.16
'@types/react-dom': ^18.0.6 '@types/react-dom': ^18.0.6
'@types/w3c-web-serial': ^1.0.2 '@types/w3c-web-serial': ^1.0.2
'@types/web-bluetooth': ^0.0.15 '@types/web-bluetooth': ^0.0.15
@ -47,7 +47,7 @@ dependencies:
'@emeraldpay/hashicon-react': 0.5.2 '@emeraldpay/hashicon-react': 0.5.2
'@hookform/resolvers': 2.9[email protected] '@hookform/resolvers': 2.9[email protected]
'@meshtastic/eslint-config': 1.0.8 '@meshtastic/eslint-config': 1.0.8
'@meshtastic/meshtasticjs': 0.6.84 '@meshtastic/meshtasticjs': 0.6.87
base64-js: 1.5.1 base64-js: 1.5.1
class-transformer: 0.5.1 class-transformer: 0.5.1
class-validator: 0.13.2 class-validator: 0.13.2
@ -72,7 +72,7 @@ devDependencies:
'@types/chrome': 0.0.193 '@types/chrome': 0.0.193
'@types/geodesy': 2.2.3 '@types/geodesy': 2.2.3
'@types/node': 18.6.4 '@types/node': 18.6.4
'@types/react': 18.0.15 '@types/react': 18.0.16
'@types/react-dom': 18.0.6 '@types/react-dom': 18.0.6
'@types/w3c-web-serial': 1.0.2 '@types/w3c-web-serial': 1.0.2
'@types/web-bluetooth': 0.0.15 '@types/web-bluetooth': 0.0.15
@ -584,8 +584,8 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@meshtastic/meshtasticjs/0.6.84: /@meshtastic/meshtasticjs/0.6.87:
resolution: {integrity: sha512-ydylXdjfTzxLh1sapDpSlkpkFR2izq9bOLP8KH4eV/4bmogk+Uc/bizLR6qJ0jQyUVcl2wFJcnOHyYwKz0jCzA==} resolution: {integrity: sha512-vehcqwhHMlUm9C/UnhmmhwVZzBjFTkYSrerwbAzJrvLC4E6tRIHCgeyPYtJZaqJoZ90gE+PD+haH2sc86mLgZQ==}
dependencies: dependencies:
'@meshtastic/eslint-config': 1.0.8 '@meshtastic/eslint-config': 1.0.8
'@protobuf-ts/runtime': 2.7.0 '@protobuf-ts/runtime': 2.7.0
@ -836,25 +836,25 @@ packages:
/@types/react-dom/18.0.6: /@types/react-dom/18.0.6:
resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==} resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==}
dependencies: dependencies:
'@types/react': 18.0.15 '@types/react': 18.0.16
dev: true dev: true
/@types/react-transition-group/4.4.5: /@types/react-transition-group/4.4.5:
resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==}
dependencies: dependencies:
'@types/react': 18.0.15 '@types/react': 18.0.16
dev: false dev: false
/@types/react/16.14.29: /@types/react/16.14.30:
resolution: {integrity: sha512-I5IwEaefGZbpmmK1J7zHwZe3JkGxcRkc5WJUDWmNySVVovueViRTEUWV7spTvpe96l3nbKD/K6+GxoN69CYb/w==} resolution: {integrity: sha512-tG+xGtDDSuIl1l63mN0LnaROAc99knkYyN4YTheE80iPzYvSy0U8LVie+OBZkrgjVrpkQV6bMCkSphPBnVNk6g==}
dependencies: dependencies:
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2 '@types/scheduler': 0.16.2
csstype: 3.1.0 csstype: 3.1.0
dev: false dev: false
/@types/react/18.0.15: /@types/react/18.0.16:
resolution: {integrity: sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==} resolution: {integrity: sha512-3vX1dzVucqc2nhXtzyaParTIIRZeNbisRqLE7QdeLomVybEyeiuAouzZXgz71P+2kbJOqj3dy0fzoATg2I06GQ==}
dependencies: dependencies:
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2 '@types/scheduler': 0.16.2
@ -1421,7 +1421,7 @@ packages:
isarray: 2.0.5 isarray: 2.0.5
object-is: 1.1.5 object-is: 1.1.5
object-keys: 1.1.1 object-keys: 1.1.1
object.assign: 4.1.2 object.assign: 4.1.3
regexp.prototype.flags: 1.4.3 regexp.prototype.flags: 1.4.3
side-channel: 1.0.4 side-channel: 1.0.4
which-boxed-primitive: 1.0.2 which-boxed-primitive: 1.0.2
@ -1556,7 +1556,7 @@ packages:
is-weakref: 1.0.2 is-weakref: 1.0.2
object-inspect: 1.12.2 object-inspect: 1.12.2
object-keys: 1.1.1 object-keys: 1.1.1
object.assign: 4.1.2 object.assign: 4.1.3
regexp.prototype.flags: 1.4.3 regexp.prototype.flags: 1.4.3
string.prototype.trimend: 1.0.5 string.prototype.trimend: 1.0.5
string.prototype.trimstart: 1.0.5 string.prototype.trimstart: 1.0.5
@ -2131,7 +2131,7 @@ packages:
dependencies: dependencies:
'@babel/runtime': 7.18.9 '@babel/runtime': 7.18.9
'@segment/react-tiny-virtual-list': 2.2[email protected] '@segment/react-tiny-virtual-list': 2.2[email protected]
'@types/react': 16.14.29 '@types/react': 16.14.30
'@types/react-transition-group': 4.4.5 '@types/react-transition-group': 4.4.5
arrify: 1.0.1 arrify: 1.0.1
classnames: 2.3.1 classnames: 2.3.1
@ -2768,7 +2768,7 @@ packages:
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
dependencies: dependencies:
array-includes: 3.1.5 array-includes: 3.1.5
object.assign: 4.1.2 object.assign: 4.1.3
dev: false dev: false
/levn/0.4.1: /levn/0.4.1:
@ -3009,8 +3009,8 @@ packages:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
/object.assign/4.1.2: /object.assign/4.1.3:
resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==} resolution: {integrity: sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
@ -3218,8 +3218,8 @@ packages:
node-modules-regexp: 1.0.0 node-modules-regexp: 1.0.0
dev: true dev: true
/postcss/8.4.14: /postcss/8.4.16:
resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} resolution: {integrity: sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
dependencies: dependencies:
nanoid: 3.3.4 nanoid: 3.3.4
@ -4055,7 +4055,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
esbuild: 0.14.53 esbuild: 0.14.53
postcss: 8.4.14 postcss: 8.4.16
resolve: 1.22.1 resolve: 1.22.1
rollup: 2.77.2 rollup: 2.77.2
optionalDependencies: optionalDependencies:

19
src/components/PageComponents/Config/Display.tsx

@ -1,8 +1,8 @@
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SelectField, TextInputField } from "evergreen-ui"; import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui";
import { useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { DisplayValidation } from "@app/validation/config/display.js"; import { DisplayValidation } from "@app/validation/config/display.js";
import { Form } from "@components/form/Form"; import { Form } from "@components/form/Form";
@ -19,6 +19,7 @@ export const Display = (): JSX.Element => {
handleSubmit, handleSubmit,
formState: { errors, isDirty }, formState: { errors, isDirty },
reset, reset,
control,
} = useForm<Protobuf.Config_DisplayConfig>({ } = useForm<Protobuf.Config_DisplayConfig>({
defaultValues: config.display, defaultValues: config.display,
resolver: classValidatorResolver(DisplayValidation), resolver: classValidatorResolver(DisplayValidation),
@ -67,6 +68,20 @@ export const Display = (): JSX.Element => {
> >
{renderOptions(Protobuf.Config_DisplayConfig_GpsCoordinateFormat)} {renderOptions(Protobuf.Config_DisplayConfig_GpsCoordinateFormat)}
</SelectField> </SelectField>
<FormField
label="Compass North Top"
description="Description"
isInvalid={!!errors.compassNorthTop?.message}
validationMessage={errors.compassNorthTop?.message}
>
<Controller
name="compassNorthTop"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
</Form> </Form>
); );
}; };

94
src/components/PageComponents/Config/WiFi copy.tsx

@ -1,94 +0,0 @@
import type React from "react";
import { useEffect, useState } from "react";
import { FormField, Switch, TextInputField, toaster } from "evergreen-ui";
import { Controller, useForm } from "react-hook-form";
import { WiFiValidation } from "@app/validation/config/wifi.js";
import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
export const WiFi = (): JSX.Element => {
const { config, connection } = useDevice();
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isDirty },
control,
reset,
} = useForm<WiFiValidation>({
defaultValues: config.wifi,
resolver: classValidatorResolver(WiFiValidation),
});
useEffect(() => {
reset(config.wifi);
}, [reset, config.wifi]);
const onSubmit = handleSubmit((data) => {
setLoading(true);
void connection?.setConfig(
{
payloadVariant: {
oneofKind: "wifi",
wifi: data,
},
},
async () => {
toaster.success("Your source is now sending data");
reset({ ...data });
setLoading(false);
await Promise.resolve();
}
);
});
return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<TextInputField
label="SSID"
description="This is a description."
isInvalid={!!errors.ssid?.message}
validationMessage={errors.ssid?.message}
{...register("ssid", { valueAsNumber: true })}
/>
<TextInputField
label="PSK"
description="This is a description."
type="password"
isInvalid={!!errors.psk?.message}
validationMessage={errors.psk?.message}
{...register("psk", { valueAsNumber: true })}
/>
<FormField
label="Enable WiFi AP"
description="Description"
isInvalid={!!errors.apMode?.message}
validationMessage={errors.apMode?.message}
>
<Controller
name="apMode"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<FormField
label="Don't broadcast SSID"
description="Description"
isInvalid={!!errors.apHidden?.message}
validationMessage={errors.apHidden?.message}
>
<Controller
name="apHidden"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
</Form>
);
};

69
src/components/PageComponents/Config/WiFi.tsx

@ -1,13 +1,21 @@
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FormField, Switch, TextInputField, toaster } from "evergreen-ui"; import {
import { Controller, useForm } from "react-hook-form"; FormField,
SelectField,
Switch,
TextInputField,
toaster,
} from "evergreen-ui";
import { Controller, useForm, useWatch } from "react-hook-form";
import { renderOptions } from "@app/core/utils/selectEnumOptions.js";
import { WiFiValidation } from "@app/validation/config/wifi.js"; import { WiFiValidation } from "@app/validation/config/wifi.js";
import { Form } from "@components/form/Form"; import { Form } from "@components/form/Form";
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
import { classValidatorResolver } from "@hookform/resolvers/class-validator"; import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs";
export const WiFi = (): JSX.Element => { export const WiFi = (): JSX.Element => {
const { config, connection } = useDevice(); const { config, connection } = useDevice();
@ -23,6 +31,12 @@ export const WiFi = (): JSX.Element => {
resolver: classValidatorResolver(WiFiValidation), resolver: classValidatorResolver(WiFiValidation),
}); });
const wifiEnabled = useWatch({
control,
name: "enabled",
defaultValue: false,
});
useEffect(() => { useEffect(() => {
reset(config.wifi); reset(config.wifi);
}, [reset, config.wifi]); }, [reset, config.wifi]);
@ -46,6 +60,27 @@ export const WiFi = (): JSX.Element => {
}); });
return ( return (
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}> <Form loading={loading} dirty={isDirty} onSubmit={onSubmit}>
<FormField
label="WiFi Enabled"
description="Description"
isInvalid={!!errors.enabled?.message}
validationMessage={errors.enabled?.message}
>
<Controller
name="enabled"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<SelectField
label="WiFi Mode"
description="This is a description."
{...register("mode", { valueAsNumber: true })}
>
{renderOptions(Protobuf.Config_WiFiConfig_WiFiMode)}
</SelectField>
<TextInputField <TextInputField
label="SSID" label="SSID"
description="This is a description." description="This is a description."
@ -53,7 +88,6 @@ export const WiFi = (): JSX.Element => {
validationMessage={errors.ssid?.message} validationMessage={errors.ssid?.message}
{...register("ssid")} {...register("ssid")}
/> />
<TextInputField <TextInputField
label="PSK" label="PSK"
type="password" type="password"
@ -62,35 +96,6 @@ export const WiFi = (): JSX.Element => {
validationMessage={errors.psk?.message} validationMessage={errors.psk?.message}
{...register("psk")} {...register("psk")}
/> />
<FormField
label="Enable WiFi AP"
description="Description"
isInvalid={!!errors.apMode?.message}
validationMessage={errors.apMode?.message}
>
<Controller
name="apMode"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
<FormField
label="Don't broadcast SSID"
description="Description"
isInvalid={!!errors.apHidden?.message}
validationMessage={errors.apHidden?.message}
>
<Controller
name="apHidden"
control={control}
render={({ field: { value, ...field } }) => (
<Switch height={24} marginLeft="auto" checked={value} {...field} />
)}
/>
</FormField>
</Form> </Form>
); );
}; };

2
src/components/layout/Sidebar/index.tsx

@ -82,6 +82,7 @@ export const Sidebar = (): JSX.Element => {
{navLinks.map((Link) => ( {navLinks.map((Link) => (
<Tab <Tab
key={Link.name} key={Link.name}
userSelect="none"
gap={majorScale(2)} gap={majorScale(2)}
disabled={Link.disabled} disabled={Link.disabled}
direction="vertical" direction="vertical"
@ -95,6 +96,7 @@ export const Sidebar = (): JSX.Element => {
</Tab> </Tab>
))} ))}
<Tab <Tab
userSelect="none"
gap={5} gap={5}
direction="vertical" direction="vertical"
isSelected={PeersDialogOpen} isSelected={PeersDialogOpen}

72
src/core/stores/deviceStore.ts

@ -14,7 +14,7 @@ export type Page =
| "info"; | "info";
export interface MessageWithAck { export interface MessageWithAck {
message: Types.TextPacket; message: Types.MessagePacket;
ack: boolean; ack: boolean;
received: Date; received: Date;
} }
@ -26,8 +26,8 @@ export interface Channel {
} }
export interface Node { export interface Node {
deviceMetrics: Protobuf.DeviceMetrics; deviceMetrics: Protobuf.DeviceMetrics[];
environmentMetrics: Protobuf.EnvironmentMetrics; environmentMetrics: Protobuf.EnvironmentMetrics[];
data: Protobuf.NodeInfo; data: Protobuf.NodeInfo;
} }
@ -40,8 +40,9 @@ export interface Device {
hardware: Protobuf.MyNodeInfo; hardware: Protobuf.MyNodeInfo;
nodes: Node[]; nodes: Node[];
connection?: IConnection; connection?: IConnection;
activeChat: number;
activePage: Page; activePage: Page;
peerInfoOpen: boolean;
activePeer: number;
setReady(ready: boolean): void; setReady(ready: boolean): void;
setStatus: (status: Types.DeviceStatusEnum) => void; setStatus: (status: Types.DeviceStatusEnum) => void;
@ -50,8 +51,9 @@ export interface Device {
setModuleConfig: (config: Protobuf.ModuleConfig) => void; setModuleConfig: (config: Protobuf.ModuleConfig) => void;
setHardware: (hardware: Protobuf.MyNodeInfo) => void; setHardware: (hardware: Protobuf.MyNodeInfo) => void;
setMetrics: (metrics: Types.TelemetryPacket) => void; setMetrics: (metrics: Types.TelemetryPacket) => void;
setActiveChat: (chatIndex: number) => void;
setActivePage: (page: Page) => void; setActivePage: (page: Page) => void;
setPeerInfoOpen: (open: boolean) => void;
setActivePeer: (peer: number) => void;
addNodeInfo: (nodeInfo: Types.NodeInfoPacket) => void; addNodeInfo: (nodeInfo: Types.NodeInfoPacket) => void;
addUser: (user: Types.UserPacket) => void; addUser: (user: Types.UserPacket) => void;
addPosition: (position: Types.PositionPacket) => void; addPosition: (position: Types.PositionPacket) => void;
@ -85,8 +87,9 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
hardware: Protobuf.MyNodeInfo.create(), hardware: Protobuf.MyNodeInfo.create(),
nodes: [], nodes: [],
connection: undefined, connection: undefined,
activeChat: 0,
activePage: "messages", activePage: "messages",
peerInfoOpen: false,
activePeer: 0,
setReady: (ready: boolean) => { setReady: (ready: boolean) => {
set( set(
@ -219,8 +222,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
snr: metrics.packet.rxSnr, snr: metrics.packet.rxSnr,
lastHeard: new Date().getSeconds(), lastHeard: new Date().getSeconds(),
}), }),
deviceMetrics: Protobuf.DeviceMetrics.create(), deviceMetrics: [],
environmentMetrics: Protobuf.EnvironmentMetrics.create(), environmentMetrics: [],
}; };
device.nodes.push(node); device.nodes.push(node);
@ -228,27 +231,20 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
if (node) { if (node) {
switch (metrics.data.variant.oneofKind) { switch (metrics.data.variant.oneofKind) {
case "deviceMetrics": case "deviceMetrics":
node.deviceMetrics = metrics.data.variant.deviceMetrics; node.deviceMetrics.push(
metrics.data.variant.deviceMetrics
);
break; break;
case "environmentMetrics": case "environmentMetrics":
node.environmentMetrics = node.environmentMetrics.push(
metrics.data.variant.environmentMetrics; metrics.data.variant.environmentMetrics
);
break; break;
} }
} }
}) })
); );
}, },
setActiveChat: (chatIndex) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.activeChat = chatIndex;
}
})
);
},
setActivePage: (page) => { setActivePage: (page) => {
set( set(
produce<DeviceState>((draft) => { produce<DeviceState>((draft) => {
@ -277,14 +273,34 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
} else { } else {
device.nodes.push({ device.nodes.push({
data: Protobuf.NodeInfo.create(nodeInfo.data), data: Protobuf.NodeInfo.create(nodeInfo.data),
deviceMetrics: Protobuf.DeviceMetrics.create(), deviceMetrics: [],
environmentMetrics: Protobuf.EnvironmentMetrics.create(), environmentMetrics: [],
}); });
} }
} }
}) })
); );
}, },
setPeerInfoOpen: (open) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.peerInfoOpen = open;
}
})
);
},
setActivePeer: (peer) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.activePeer = peer;
}
})
);
},
addUser: (user) => { addUser: (user) => {
set( set(
produce<DeviceState>((draft) => { produce<DeviceState>((draft) => {
@ -307,8 +323,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
snr: user.packet.rxSnr, snr: user.packet.rxSnr,
user: user.data, user: user.data,
}), }),
deviceMetrics: Protobuf.DeviceMetrics.create(), deviceMetrics: [],
environmentMetrics: Protobuf.EnvironmentMetrics.create(), environmentMetrics: [],
}); });
} }
} }
@ -336,8 +352,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
num: position.packet.from, num: position.packet.from,
position: position.data, position: position.data,
}), }),
deviceMetrics: Protobuf.DeviceMetrics.create(), deviceMetrics: [],
environmentMetrics: Protobuf.EnvironmentMetrics.create(), environmentMetrics: [],
}); });
} }
} }
@ -369,6 +385,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
); );
}, },
ackMessage: (channelIndex: number, messageId: number) => { ackMessage: (channelIndex: number, messageId: number) => {
console.log("ack called");
set( set(
produce<DeviceState>((draft) => { produce<DeviceState>((draft) => {
const device = draft.devices.get(id); const device = draft.devices.get(id);

14
src/core/subscriptions.ts

@ -12,14 +12,14 @@ export const subscribeAll = (device: Device, connection: IConnection) => {
// onLogEvent // onLogEvent
// onMeshHeartbeat // onMeshHeartbeat
// onRoutingPacket
// onTelemetryPacket
connection.onRoutingPacket.subscribe((routingPacket) => { connection.onRoutingPacket.subscribe((routingPacket) => {
console.log(routingPacket); console.log(routingPacket);
}); });
connection.onTelemetryPacket.subscribe((telemetryPacket) => { connection.onTelemetryPacket.subscribe((telemetryPacket) => {
console.log(telemetryPacket.data.variant);
device.setMetrics(telemetryPacket); device.setMetrics(telemetryPacket);
}); });
@ -63,12 +63,12 @@ export const subscribeAll = (device: Device, connection: IConnection) => {
device.setModuleConfig(moduleConfig.data); device.setModuleConfig(moduleConfig.data);
}); });
connection.onTextPacket.subscribe((message) => { connection.onMessagePacket.subscribe((messagePacket) => {
device.addMessage({ device.addMessage({
message: message, message: messagePacket,
ack: message.packet.from !== device.hardware.myNodeNum, ack: messagePacket.packet.from !== device.hardware.myNodeNum,
received: message.packet.rxTime received: messagePacket.packet.rxTime
? new Date(message.packet.rxTime * 1000) ? new Date(messagePacket.packet.rxTime * 1000)
: new Date(), : new Date(),
}); });
}); });

5
src/pages/Info/index.tsx

@ -1,6 +1,7 @@
import type React from "react"; import type React from "react";
import { Pane } from "evergreen-ui"; import { Pane } from "evergreen-ui";
import JSONPretty from "react-json-pretty";
import { useDevice } from "@app/core/stores/deviceStore.js"; import { useDevice } from "@app/core/stores/deviceStore.js";
@ -11,8 +12,8 @@ export const InfoPage = (): JSX.Element => {
return ( return (
<Pane> <Pane>
{JSON.stringify(myNode)} <JSONPretty data={myNode} />
{JSON.stringify(hardware)} <JSONPretty data={hardware} />
</Pane> </Pane>
); );
}; };

33
src/pages/Messages/LocationMessage.tsx

@ -0,0 +1,33 @@
import type React from "react";
import { LocateIcon, majorScale, minorScale, Pane, Text } from "evergreen-ui";
import { toMGRS } from "@app/core/utils/toMGRS.js";
import type { Protobuf } from "@meshtastic/meshtasticjs";
export interface LocationMessageProps {
location: Protobuf.Location;
}
export const LocationMessage = ({
location,
}: LocationMessageProps): JSX.Element => {
return (
<Pane
marginLeft={majorScale(2)}
paddingLeft={majorScale(1)}
borderLeft="3px solid #e6e6e6"
>
<Pane
gap={majorScale(1)}
display="flex"
borderRadius={majorScale(1)}
elevation={1}
padding={minorScale(1)}
>
<LocateIcon color="#474d66" marginY="auto" />
<Text>{toMGRS(location.latitudeI, location.longitudeI)}</Text>
</Pane>
</Pane>
);
};

130
src/pages/Messages/Message.tsx

@ -1,21 +1,23 @@
import type React from "react"; import type React from "react";
import { import {
CircleIcon,
FullCircleIcon,
majorScale, majorScale,
Pane, Pane,
Pulsar,
Small, Small,
Strong, Strong,
Text, Text,
TimeIcon,
} from "evergreen-ui"; } from "evergreen-ui";
import { Hashicon } from "@emeraldpay/hashicon-react"; import { Hashicon } from "@emeraldpay/hashicon-react";
import type { Protobuf } from "@meshtastic/meshtasticjs"; import type { Protobuf, Types } from "@meshtastic/meshtasticjs";
import { LocationMessage } from "./LocationMessage.js";
export interface MessageProps { export interface MessageProps {
lastMsgSameUser: boolean; lastMsgSameUser: boolean;
message: string; messagePacket: Types.MessagePacket;
ack: boolean; ack: boolean;
rxTime: Date; rxTime: Date;
sender?: Protobuf.NodeInfo; sender?: Protobuf.NodeInfo;
@ -23,78 +25,66 @@ export interface MessageProps {
export const Message = ({ export const Message = ({
lastMsgSameUser, lastMsgSameUser,
message, messagePacket,
ack, ack,
rxTime, rxTime,
sender, sender,
}: MessageProps): JSX.Element => { }: MessageProps): JSX.Element => {
return ( return lastMsgSameUser ? (
<Pane marginBottom={majorScale(1)} className="group hover:bg-gray-200"> <Pane display="flex" marginLeft={majorScale(3)}>
{lastMsgSameUser ? ( {ack ? (
<Pane <FullCircleIcon color="#9c9fab" marginY="auto" size={8} />
marginX={majorScale(2)}
display="flex"
justifyContent="space-between"
marginTop={-majorScale(1)}
className={`${lastMsgSameUser ? "" : "py-1"}`}
>
<Pane
display="flex"
position="relative"
gap={majorScale(1)}
className="gap-2"
>
<Small
marginY="auto"
marginLeft="auto"
width={majorScale(3)}
paddingTop={majorScale(1)}
className="pt-1 text-transparent group-hover:text-gray-500"
>
{rxTime
.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})
.replace("AM", "")
.replace("PM", "")}
</Small>
<Text marginY="auto" className={`${ack ? "" : "animate-pulse"}`}>
{message}
</Text>
<Pulsar />
</Pane>
<Pane
display="flex"
gap={majorScale(1)}
paddingTop={majorScale(1)}
className="text-transparent group-hover:text-gray-500"
>
<TimeIcon />
<Small>25s</Small>
</Pane>
</Pane>
) : ( ) : (
<Pane display="flex" marginX={majorScale(2)} gap={majorScale(1)}> <CircleIcon color="#9c9fab" marginY="auto" size={8} />
<Pane marginY="auto" width={majorScale(3)}>
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
</Pane>
<Pane>
<Pane display="flex" gap={majorScale(1)}>
<Strong cursor="default" size={500} className="hover:underline">
{sender?.user?.longName ?? "UNK"}
</Strong>
<Small marginY="auto">
{rxTime.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})}
</Small>
</Pane>
<Text className={`${ack ? "" : "animate-pulse"}`}>{message}</Text>
</Pane>
</Pane>
)} )}
{messagePacket.location ? (
<LocationMessage location={messagePacket.location} />
) : (
<Text
color={ack ? "#474d66" : "#9c9fab"}
marginLeft={majorScale(2)}
paddingLeft={majorScale(1)}
borderLeft="3px solid #e6e6e6"
>
{messagePacket.text}
</Text>
)}
</Pane>
) : (
<Pane marginX={majorScale(2)} gap={majorScale(1)} marginTop={majorScale(1)}>
<Pane display="flex" gap={majorScale(1)}>
<Pane width={majorScale(3)}>
<Hashicon value={(sender?.num ?? 0).toString()} size={32} />
</Pane>
<Strong cursor="default" size={500}>
{sender?.user?.longName ?? "UNK"}
</Strong>
<Small>
{rxTime.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})}
</Small>
</Pane>
<Pane display="flex" marginLeft={majorScale(1)}>
{ack ? (
<FullCircleIcon color="#9c9fab" marginY="auto" size={8} />
) : (
<CircleIcon color="#9c9fab" marginY="auto" size={8} />
)}
{messagePacket.location ? (
<LocationMessage location={messagePacket.location} />
) : (
<Text
color={ack ? "#474d66" : "#9c9fab"}
marginLeft={majorScale(2)}
paddingLeft={majorScale(1)}
borderLeft="3px solid #e6e6e6"
>
{messagePacket.text}
</Text>
)}
</Pane>
</Pane> </Pane>
); );
}; };

5
src/validation/config/display.ts

@ -1,4 +1,4 @@
import { IsEnum, IsInt } from "class-validator"; import { IsBoolean, IsEnum, IsInt } from "class-validator";
import { Protobuf } from "@meshtastic/meshtasticjs"; import { Protobuf } from "@meshtastic/meshtasticjs";
@ -11,4 +11,7 @@ export class DisplayValidation implements Protobuf.Config_DisplayConfig {
@IsInt() @IsInt()
autoScreenCarouselSecs: number; autoScreenCarouselSecs: number;
@IsBoolean()
compassNorthTop: boolean;
} }

14
src/validation/config/wifi.ts

@ -1,15 +1,17 @@
import { Length } from "class-validator"; import { IsBoolean, IsEnum, Length } from "class-validator";
import type { Protobuf } from "@meshtastic/meshtasticjs"; import { Protobuf } from "@meshtastic/meshtasticjs";
export class WiFiValidation implements Protobuf.Config_WiFiConfig { export class WiFiValidation implements Protobuf.Config_WiFiConfig {
@IsBoolean()
enabled: boolean;
@IsEnum(Protobuf.Config_WiFiConfig_WiFiMode)
mode: Protobuf.Config_WiFiConfig_WiFiMode;
@Length(1, 33) @Length(1, 33)
ssid: string; ssid: string;
@Length(8, 64) @Length(8, 64)
psk: string; psk: string;
apMode: boolean;
apHidden: boolean;
} }

Loading…
Cancel
Save