52 changed files with 829 additions and 1261 deletions
@ -8,8 +8,7 @@ |
|||
"build": "tsc && vite build", |
|||
"preview": "vite preview", |
|||
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)", |
|||
"format": "prettier --write 'src/**/*.{ts,tsx}' && eslint src/*.{ts,tsx}", |
|||
"check": "unimported" |
|||
"format": "prettier --write 'src/**/*.{ts,tsx}' && eslint src/*.{ts,tsx}" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
@ -20,35 +19,34 @@ |
|||
}, |
|||
"homepage": "https://meshtastic.org", |
|||
"dependencies": { |
|||
"@arcgis/core": "^4.24.7", |
|||
"@emeraldpay/hashicon-react": "^0.5.2", |
|||
"@hookform/resolvers": "^2.9.7", |
|||
"@meshtastic/eslint-config": "^1.0.8", |
|||
"@meshtastic/meshtasticjs": "^0.6.88", |
|||
"@meshtastic/meshtasticjs": "^0.6.92", |
|||
"base64-js": "^1.5.1", |
|||
"class-transformer": "^0.5.1", |
|||
"class-validator": "^0.13.2", |
|||
"evergreen-ui": "^6.10.3", |
|||
"geodesy": "^2.4.0", |
|||
"immer": "^9.0.15", |
|||
"mapbox-gl": "npm:[email protected]", |
|||
"maplibre-gl": "^2.3.0", |
|||
"modern-css-reset": "^1.4.0", |
|||
"prettier": "^2.7.1", |
|||
"react": "^18.2.0", |
|||
"react-dom": "^18.2.0", |
|||
"react-hook-form": "^7.34.0", |
|||
"react-hook-form": "^7.34.2", |
|||
"react-icons": "^4.4.0", |
|||
"react-json-pretty": "^2.2.0", |
|||
"react-qrcode-logo": "^2.7.0", |
|||
"react-map-gl": "^7.0.19", |
|||
"react-qrcode-logo": "^2.8.0", |
|||
"rfc4648": "^1.5.2", |
|||
"snarkdown": "^2.0.0", |
|||
"swr": "^1.3.0", |
|||
"vite-plugin-environment": "^1.1.2", |
|||
"zustand": "4.0.0" |
|||
"zustand": "4.1.0" |
|||
}, |
|||
"devDependencies": { |
|||
"@types/chrome": "^0.0.193", |
|||
"@types/geodesy": "^2.2.3", |
|||
"@types/node": "^18.7.1", |
|||
"@types/node": "^18.7.6", |
|||
"@types/react": "^18.0.17", |
|||
"@types/react-dom": "^18.0.6", |
|||
"@types/w3c-web-serial": "^1.0.2", |
|||
@ -59,8 +57,7 @@ |
|||
"tar": "^6.1.11", |
|||
"tslib": "^2.4.0", |
|||
"typescript": "^4.7.4", |
|||
"unimported": "^1.21.0", |
|||
"vite": "^3.0.6", |
|||
"vite-plugin-cdn-import": "^0.3.5" |
|||
"vite": "^3.0.8", |
|||
"vite-plugin-environment": "^1.1.2" |
|||
} |
|||
} |
|||
|
|||
File diff suppressed because it is too large
@ -0,0 +1,7 @@ |
|||
import type React from "react"; |
|||
|
|||
import { Pane } from "evergreen-ui"; |
|||
|
|||
export const HelpDialog = (): JSX.Element => { |
|||
return <Pane></Pane>; |
|||
}; |
|||
@ -0,0 +1,100 @@ |
|||
import type React from "react"; |
|||
import { useEffect, useState } from "react"; |
|||
|
|||
import { FormField, SelectField, Switch, TextInputField } from "evergreen-ui"; |
|||
import { Controller, useForm, useWatch } from "react-hook-form"; |
|||
|
|||
import { BluetoothValidation } from "@app/validation/config/bluetooth.js"; |
|||
import { Form } from "@components/form/Form"; |
|||
import { useDevice } from "@core/providers/useDevice.js"; |
|||
import { renderOptions } from "@core/utils/selectEnumOptions.js"; |
|||
import { classValidatorResolver } from "@hookform/resolvers/class-validator"; |
|||
import { Protobuf } from "@meshtastic/meshtasticjs"; |
|||
|
|||
export const Bluetooth = (): JSX.Element => { |
|||
const { config, connection } = useDevice(); |
|||
const [loading, setLoading] = useState(false); |
|||
|
|||
const { |
|||
register, |
|||
handleSubmit, |
|||
formState: { errors, isDirty }, |
|||
control, |
|||
reset, |
|||
} = useForm<BluetoothValidation>({ |
|||
defaultValues: config.bluetooth, |
|||
resolver: classValidatorResolver(BluetoothValidation), |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
reset(config.bluetooth); |
|||
}, [reset, config.bluetooth]); |
|||
|
|||
const onSubmit = handleSubmit((data) => { |
|||
setLoading(true); |
|||
void connection?.setConfig( |
|||
{ |
|||
payloadVariant: { |
|||
oneofKind: "bluetooth", |
|||
bluetooth: data, |
|||
}, |
|||
}, |
|||
async () => { |
|||
reset({ ...data }); |
|||
setLoading(false); |
|||
await Promise.resolve(); |
|||
} |
|||
); |
|||
}); |
|||
|
|||
const pairingMode = useWatch({ |
|||
control, |
|||
name: "mode", |
|||
defaultValue: Protobuf.Config_BluetoothConfig_PairingMode.RandomPin, |
|||
}); |
|||
|
|||
return ( |
|||
<Form loading={loading} dirty={isDirty} onSubmit={onSubmit}> |
|||
<FormField |
|||
label="Bluetooth 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="Pairing mode" |
|||
description="This is a description." |
|||
isInvalid={!!errors.mode?.message} |
|||
validationMessage={errors.mode?.message} |
|||
{...register("mode", { valueAsNumber: true })} |
|||
> |
|||
{renderOptions(Protobuf.Config_BluetoothConfig_PairingMode)} |
|||
</SelectField> |
|||
|
|||
<TextInputField |
|||
display={ |
|||
pairingMode !== Protobuf.Config_BluetoothConfig_PairingMode.FixedPin |
|||
? "none" |
|||
: "block" |
|||
} |
|||
label="Pin" |
|||
description="This is a description." |
|||
type="number" |
|||
isInvalid={!!errors.fixedPin?.message} |
|||
validationMessage={errors.fixedPin?.message} |
|||
{...register("fixedPin", { |
|||
valueAsNumber: true, |
|||
})} |
|||
/> |
|||
</Form> |
|||
); |
|||
}; |
|||
@ -0,0 +1,13 @@ |
|||
import { createContext, useContext } from "react"; |
|||
|
|||
import type { Device } from "../stores/deviceStore.js"; |
|||
|
|||
export const DeviceContext = createContext<Device | undefined>(undefined); |
|||
|
|||
export const useDevice = (): Device => { |
|||
const context = useContext(DeviceContext); |
|||
if (context === undefined) { |
|||
throw new Error("useDevice must be used within a ConnectionProvider"); |
|||
} |
|||
return context; |
|||
}; |
|||
@ -1,7 +0,0 @@ |
|||
export const fetcher = async <JSON>( |
|||
input: RequestInfo, |
|||
init?: RequestInit |
|||
): Promise<JSON> => { |
|||
const res = await fetch(input, init); |
|||
return res.json() as Promise<JSON>; |
|||
}; |
|||
@ -1,137 +1,112 @@ |
|||
import type React from "react"; |
|||
import { useEffect, useMemo, useRef } from "react"; |
|||
|
|||
import { Pane } from "evergreen-ui"; |
|||
import { |
|||
Heading, |
|||
IconButton, |
|||
LocateIcon, |
|||
majorScale, |
|||
MapMarkerIcon, |
|||
Pane, |
|||
Text, |
|||
} from "evergreen-ui"; |
|||
import maplibregl from "maplibre-gl"; |
|||
import { Map, Marker, useMap } from "react-map-gl"; |
|||
|
|||
import { useDevice } from "@app/core/stores/deviceStore.js"; |
|||
import Point from "@arcgis/core/geometry/Point"; |
|||
import Graphic from "@arcgis/core/Graphic"; |
|||
import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; |
|||
import LabelClass from "@arcgis/core/layers/support/LabelClass"; |
|||
import Map from "@arcgis/core/Map"; |
|||
import LineCallout3D from "@arcgis/core/symbols/callouts/LineCallout3D"; |
|||
import LabelSymbol3D from "@arcgis/core/symbols/LabelSymbol3D"; |
|||
import TextSymbol3DLayer from "@arcgis/core/symbols/TextSymbol3DLayer"; |
|||
import SceneView from "@arcgis/core/views/SceneView"; |
|||
import { useDevice } from "@core/providers/useDevice.js"; |
|||
import { Hashicon } from "@emeraldpay/hashicon-react"; |
|||
|
|||
export const MapPage = (): JSX.Element => { |
|||
const { nodes } = useDevice(); |
|||
const { nodes, waypoints } = useDevice(); |
|||
const { current: map } = useMap(); |
|||
|
|||
const nodesWithPosition = nodes.filter((node) => node.data.position); |
|||
const ref = useRef<HTMLDivElement>(null); |
|||
return ( |
|||
<Pane |
|||
margin={majorScale(3)} |
|||
borderRadius={majorScale(1)} |
|||
elevation={1} |
|||
display="flex" |
|||
flexGrow={1} |
|||
flexDirection="column" |
|||
gap={majorScale(2)} |
|||
overflow="hidden" |
|||
position="relative" |
|||
> |
|||
<Pane |
|||
position="absolute" |
|||
zIndex={10} |
|||
right={0} |
|||
top={0} |
|||
borderRadius={majorScale(1)} |
|||
padding={majorScale(1)} |
|||
margin={majorScale(1)} |
|||
background="tint1" |
|||
width={majorScale(28)} |
|||
elevation={1} |
|||
overflow="hidden" |
|||
> |
|||
<Pane padding={majorScale(1)} background="tint2"> |
|||
<Heading>Title</Heading> |
|||
</Pane> |
|||
<Pane display="flex" flexDirection="column" gap={majorScale(1)}> |
|||
{nodes.map((n) => ( |
|||
<Pane key={n.data.num} display="flex" gap={majorScale(1)}> |
|||
<Hashicon value={n.data.num.toString()} size={24} /> |
|||
<Text>{n.data.user?.longName}</Text> |
|||
<IconButton |
|||
icon={LocateIcon} |
|||
marginLeft="auto" |
|||
size="small" |
|||
onClick={() => { |
|||
console.log("clicked"); |
|||
console.log(map); |
|||
|
|||
useEffect(() => { |
|||
console.log(nodesWithPosition); |
|||
}, [nodesWithPosition]); |
|||
|
|||
const labelClass = useMemo( |
|||
() => |
|||
new LabelClass({ |
|||
labelExpressionInfo: { |
|||
expression: "$feature.name", |
|||
}, |
|||
symbol: new LabelSymbol3D({ |
|||
symbolLayers: [ |
|||
new TextSymbol3DLayer({ |
|||
text: "{name}", |
|||
material: { |
|||
color: "black", |
|||
}, |
|||
halo: { |
|||
color: [255, 255, 255, 0.7], |
|||
size: 2, |
|||
}, |
|||
font: { |
|||
size: 12, |
|||
weight: "bold", |
|||
}, |
|||
size: 10, |
|||
}), |
|||
], |
|||
verticalOffset: { |
|||
screenLength: 150, |
|||
maxWorldLength: 2000, |
|||
minWorldLength: 30, |
|||
}, |
|||
callout: new LineCallout3D({ |
|||
size: 0.5, |
|||
color: [0, 0, 0], |
|||
border: { |
|||
color: [255, 255, 255], |
|||
}, |
|||
}), |
|||
}), |
|||
}), |
|||
[] |
|||
); |
|||
|
|||
const points: Graphic[] = nodesWithPosition.map( |
|||
(node, index) => |
|||
node.data.position |
|||
? new Graphic({ |
|||
geometry: new Point({ |
|||
latitude: node.data.position.latitudeI / 1e7, |
|||
longitude: node.data.position.longitudeI / 1e7, |
|||
}), |
|||
attributes: { |
|||
ObjectID: index, |
|||
name: node.data.user?.longName, |
|||
}, |
|||
}) |
|||
: new Graphic() //should be undefined/removed from array
|
|||
map?.flyTo({ |
|||
center: [ |
|||
n.data.position?.latitudeI / 1e7, |
|||
n.data.position?.longitudeI / 1e7, |
|||
], |
|||
zoom: 10, |
|||
}); |
|||
}} |
|||
/> |
|||
</Pane> |
|||
))} |
|||
</Pane> |
|||
</Pane> |
|||
<Map |
|||
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json" |
|||
mapLib={maplibregl} |
|||
attributionControl={false} |
|||
> |
|||
{waypoints.map((wp) => ( |
|||
<Marker |
|||
key={wp.id} |
|||
longitude={wp.longitudeI / 1e7} |
|||
latitude={wp.latitudeI / 1e7} |
|||
anchor="bottom" |
|||
> |
|||
<Pane> |
|||
<MapMarkerIcon /> |
|||
</Pane> |
|||
</Marker> |
|||
))} |
|||
{nodes |
|||
.filter((n) => n.data.position?.latitudeI) |
|||
.map((n) => { |
|||
if (n.data.position?.latitudeI) { |
|||
return ( |
|||
<Marker |
|||
key={n.data.num} |
|||
longitude={n.data.position.longitudeI / 1e7} |
|||
latitude={n.data.position.latitudeI / 1e7} |
|||
anchor="bottom" |
|||
> |
|||
<Hashicon value={n.data.num.toString()} size={32} /> |
|||
</Marker> |
|||
); |
|||
} |
|||
})} |
|||
</Map> |
|||
</Pane> |
|||
); |
|||
|
|||
useEffect(() => { |
|||
if (ref.current) { |
|||
const layer = new FeatureLayer({ |
|||
labelsVisible: true, |
|||
labelingInfo: [labelClass], |
|||
source: points, |
|||
fields: [ |
|||
{ |
|||
name: "ObjectID", |
|||
alias: "ObjectID", |
|||
type: "oid", |
|||
}, |
|||
{ |
|||
name: "name", |
|||
alias: "Name", |
|||
type: "string", |
|||
}, |
|||
], |
|||
}); |
|||
|
|||
const map = new Map({ |
|||
basemap: "satellite", |
|||
ground: "world-elevation", |
|||
layers: [layer], |
|||
}); |
|||
|
|||
const scene = new SceneView({ |
|||
container: ref.current, |
|||
map: map, |
|||
camera: { |
|||
position: nodesWithPosition[0] |
|||
? { |
|||
x: nodesWithPosition[0].data.position?.longitudeI ?? 0 / 1e7, |
|||
y: nodesWithPosition[0].data.position?.latitudeI ?? 0 / 1e7, |
|||
z: nodesWithPosition[0].data.position?.altitude ?? 0 / 1e7, |
|||
} |
|||
: { |
|||
y: -35.59, //Longitude
|
|||
x: 148, //Latitude
|
|||
z: 200, //Meters
|
|||
}, |
|||
tilt: 75, |
|||
}, |
|||
}); |
|||
scene.on("click", (event) => { |
|||
void scene.hitTest(event).then((point) => { |
|||
console.log(point); |
|||
}); |
|||
}); |
|||
} |
|||
}, [labelClass, points]); |
|||
|
|||
return <Pane width="100%" height="100%" ref={ref} />; |
|||
}; |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
import { IsBoolean, IsEnum, IsInt } from "class-validator"; |
|||
|
|||
import { Protobuf } from "@meshtastic/meshtasticjs"; |
|||
|
|||
export class BluetoothValidation implements Protobuf.Config_BluetoothConfig { |
|||
@IsBoolean() |
|||
enabled: boolean; |
|||
|
|||
@IsEnum(Protobuf.Config_BluetoothConfig_PairingMode) |
|||
mode: Protobuf.Config_BluetoothConfig_PairingMode; |
|||
|
|||
@IsInt() |
|||
fixedPin: number; |
|||
} |
|||
Loading…
Reference in new issue