52 changed files with 829 additions and 1261 deletions
@ -8,8 +8,7 @@ |
|||||
"build": "tsc && vite build", |
"build": "tsc && vite build", |
||||
"preview": "vite preview", |
"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/)", |
"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}", |
"format": "prettier --write 'src/**/*.{ts,tsx}' && eslint src/*.{ts,tsx}" |
||||
"check": "unimported" |
|
||||
}, |
}, |
||||
"repository": { |
"repository": { |
||||
"type": "git", |
"type": "git", |
||||
@ -20,35 +19,34 @@ |
|||||
}, |
}, |
||||
"homepage": "https://meshtastic.org", |
"homepage": "https://meshtastic.org", |
||||
"dependencies": { |
"dependencies": { |
||||
"@arcgis/core": "^4.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.88", |
"@meshtastic/meshtasticjs": "^0.6.92", |
||||
"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", |
||||
"evergreen-ui": "^6.10.3", |
"evergreen-ui": "^6.10.3", |
||||
"geodesy": "^2.4.0", |
"geodesy": "^2.4.0", |
||||
"immer": "^9.0.15", |
"immer": "^9.0.15", |
||||
|
"mapbox-gl": "npm:[email protected]", |
||||
|
"maplibre-gl": "^2.3.0", |
||||
"modern-css-reset": "^1.4.0", |
"modern-css-reset": "^1.4.0", |
||||
"prettier": "^2.7.1", |
"prettier": "^2.7.1", |
||||
"react": "^18.2.0", |
"react": "^18.2.0", |
||||
"react-dom": "^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-icons": "^4.4.0", |
||||
"react-json-pretty": "^2.2.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", |
"rfc4648": "^1.5.2", |
||||
"snarkdown": "^2.0.0", |
"zustand": "4.1.0" |
||||
"swr": "^1.3.0", |
|
||||
"vite-plugin-environment": "^1.1.2", |
|
||||
"zustand": "4.0.0" |
|
||||
}, |
}, |
||||
"devDependencies": { |
"devDependencies": { |
||||
"@types/chrome": "^0.0.193", |
"@types/chrome": "^0.0.193", |
||||
"@types/geodesy": "^2.2.3", |
"@types/geodesy": "^2.2.3", |
||||
"@types/node": "^18.7.1", |
"@types/node": "^18.7.6", |
||||
"@types/react": "^18.0.17", |
"@types/react": "^18.0.17", |
||||
"@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", |
||||
@ -59,8 +57,7 @@ |
|||||
"tar": "^6.1.11", |
"tar": "^6.1.11", |
||||
"tslib": "^2.4.0", |
"tslib": "^2.4.0", |
||||
"typescript": "^4.7.4", |
"typescript": "^4.7.4", |
||||
"unimported": "^1.21.0", |
"vite": "^3.0.8", |
||||
"vite": "^3.0.6", |
"vite-plugin-environment": "^1.1.2" |
||||
"vite-plugin-cdn-import": "^0.3.5" |
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
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 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 { useDevice } from "@core/providers/useDevice.js"; |
||||
import Point from "@arcgis/core/geometry/Point"; |
import { Hashicon } from "@emeraldpay/hashicon-react"; |
||||
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"; |
|
||||
|
|
||||
export const MapPage = (): JSX.Element => { |
export const MapPage = (): JSX.Element => { |
||||
const { nodes } = useDevice(); |
const { nodes, waypoints } = useDevice(); |
||||
|
const { current: map } = useMap(); |
||||
|
|
||||
const nodesWithPosition = nodes.filter((node) => node.data.position); |
return ( |
||||
const ref = useRef<HTMLDivElement>(null); |
<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(() => { |
map?.flyTo({ |
||||
console.log(nodesWithPosition); |
center: [ |
||||
}, [nodesWithPosition]); |
n.data.position?.latitudeI / 1e7, |
||||
|
n.data.position?.longitudeI / 1e7, |
||||
const labelClass = useMemo( |
], |
||||
() => |
zoom: 10, |
||||
new LabelClass({ |
}); |
||||
labelExpressionInfo: { |
}} |
||||
expression: "$feature.name", |
/> |
||||
}, |
</Pane> |
||||
symbol: new LabelSymbol3D({ |
))} |
||||
symbolLayers: [ |
</Pane> |
||||
new TextSymbol3DLayer({ |
</Pane> |
||||
text: "{name}", |
<Map |
||||
material: { |
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json" |
||||
color: "black", |
mapLib={maplibregl} |
||||
}, |
attributionControl={false} |
||||
halo: { |
> |
||||
color: [255, 255, 255, 0.7], |
{waypoints.map((wp) => ( |
||||
size: 2, |
<Marker |
||||
}, |
key={wp.id} |
||||
font: { |
longitude={wp.longitudeI / 1e7} |
||||
size: 12, |
latitude={wp.latitudeI / 1e7} |
||||
weight: "bold", |
anchor="bottom" |
||||
}, |
> |
||||
size: 10, |
<Pane> |
||||
}), |
<MapMarkerIcon /> |
||||
], |
</Pane> |
||||
verticalOffset: { |
</Marker> |
||||
screenLength: 150, |
))} |
||||
maxWorldLength: 2000, |
{nodes |
||||
minWorldLength: 30, |
.filter((n) => n.data.position?.latitudeI) |
||||
}, |
.map((n) => { |
||||
callout: new LineCallout3D({ |
if (n.data.position?.latitudeI) { |
||||
size: 0.5, |
return ( |
||||
color: [0, 0, 0], |
<Marker |
||||
border: { |
key={n.data.num} |
||||
color: [255, 255, 255], |
longitude={n.data.position.longitudeI / 1e7} |
||||
}, |
latitude={n.data.position.latitudeI / 1e7} |
||||
}), |
anchor="bottom" |
||||
}), |
> |
||||
}), |
<Hashicon value={n.data.num.toString()} size={32} /> |
||||
[] |
</Marker> |
||||
); |
); |
||||
|
} |
||||
const points: Graphic[] = nodesWithPosition.map( |
})} |
||||
(node, index) => |
</Map> |
||||
node.data.position |
</Pane> |
||||
? 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
|
|
||||
); |
); |
||||
|
|
||||
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