44 changed files with 631 additions and 1084 deletions
File diff suppressed because it is too large
@ -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,32 @@ |
|||||
|
import type React from "react"; |
||||
|
import { useEffect, useState } from "react"; |
||||
|
|
||||
|
import { Map, MapOptions } from "maplibre-gl"; |
||||
|
|
||||
|
export interface useMapboxProps { |
||||
|
ref: React.RefObject<HTMLDivElement>; |
||||
|
style: string; |
||||
|
options?: Partial<MapOptions>; |
||||
|
} |
||||
|
|
||||
|
export function useCreateMapbox({ |
||||
|
ref, |
||||
|
style, |
||||
|
options, |
||||
|
}: useMapboxProps): Map | undefined { |
||||
|
const [mapInstance, setMapInstance] = useState<Map>(); |
||||
|
useEffect(() => { |
||||
|
const container = ref.current as HTMLDivElement; |
||||
|
if (mapInstance || !container) { |
||||
|
return; |
||||
|
} |
||||
|
const map = new Map({ |
||||
|
container, |
||||
|
style, |
||||
|
...options, |
||||
|
}); |
||||
|
setMapInstance(map); |
||||
|
}, []); |
||||
|
|
||||
|
return mapInstance; |
||||
|
} |
||||
@ -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; |
||||
|
}; |
||||
@ -0,0 +1,14 @@ |
|||||
|
import { createContext, useContext } from "react"; |
||||
|
|
||||
|
import type { Map } from "maplibre-gl"; |
||||
|
|
||||
|
export interface MapContextValue { |
||||
|
ref: React.Ref<HTMLDivElement>; |
||||
|
map?: Map; |
||||
|
} |
||||
|
|
||||
|
export const MapContext = createContext<MapContextValue>({} as MapContextValue); |
||||
|
|
||||
|
export const useMap = (): MapContextValue => { |
||||
|
return useContext(MapContext); |
||||
|
}; |
||||
@ -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,63 @@ |
|||||
import type React from "react"; |
import type React from "react"; |
||||
import { useEffect, useMemo, useRef } from "react"; |
import { useEffect, useMemo, useRef } from "react"; |
||||
|
|
||||
import { Pane } from "evergreen-ui"; |
import { majorScale, Pane } from "evergreen-ui"; |
||||
|
import { Marker } from "maplibre-gl"; |
||||
|
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js"; |
import { useCreateMapbox } from "@app/core/providers/useCreateMapbox.js"; |
||||
import Point from "@arcgis/core/geometry/Point"; |
import { useDevice } from "@core/providers/useDevice.js"; |
||||
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 } = useDevice(); |
||||
|
|
||||
const nodesWithPosition = nodes.filter((node) => node.data.position); |
const nodeMarkers = useMemo(() => new Map<number, Marker>(), []); |
||||
const ref = useRef<HTMLDivElement>(null); |
|
||||
|
|
||||
useEffect(() => { |
|
||||
console.log(nodesWithPosition); |
|
||||
}, [nodesWithPosition]); |
|
||||
|
|
||||
const labelClass = useMemo( |
const ref = useRef<HTMLDivElement>(null); |
||||
() => |
|
||||
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( |
const map = useCreateMapbox({ |
||||
(node, index) => |
ref, |
||||
node.data.position |
style: |
||||
? new Graphic({ |
"https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json", |
||||
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(() => { |
useEffect(() => { |
||||
if (ref.current) { |
nodes.map((n) => { |
||||
const layer = new FeatureLayer({ |
if (n.data.position?.longitudeI && n.data.position?.latitudeI && map) { |
||||
labelsVisible: true, |
if (nodeMarkers.has(n.data.num)) { |
||||
labelingInfo: [labelClass], |
nodeMarkers |
||||
source: points, |
.get(n.data.num) |
||||
fields: [ |
?.setLngLat([ |
||||
{ |
n.data.position?.longitudeI / 1e7, |
||||
name: "ObjectID", |
n.data.position?.latitudeI / 1e7, |
||||
alias: "ObjectID", |
]); |
||||
type: "oid", |
} else { |
||||
}, |
nodeMarkers.set( |
||||
{ |
n.data.num, |
||||
name: "name", |
new Marker() |
||||
alias: "Name", |
.setLngLat([ |
||||
type: "string", |
n.data.position?.longitudeI / 1e7, |
||||
}, |
n.data.position?.latitudeI / 1e7, |
||||
], |
]) |
||||
}); |
.addTo(map) |
||||
|
); |
||||
const map = new Map({ |
} |
||||
basemap: "satellite", |
} |
||||
ground: "world-elevation", |
}); |
||||
layers: [layer], |
}, [map, nodeMarkers, nodes]); |
||||
}); |
|
||||
|
return ( |
||||
const scene = new SceneView({ |
<Pane |
||||
container: ref.current, |
margin={majorScale(3)} |
||||
map: map, |
borderRadius={majorScale(1)} |
||||
camera: { |
background="white" |
||||
position: nodesWithPosition[0] |
elevation={1} |
||||
? { |
display="flex" |
||||
x: nodesWithPosition[0].data.position?.longitudeI ?? 0 / 1e7, |
flexGrow={1} |
||||
y: nodesWithPosition[0].data.position?.latitudeI ?? 0 / 1e7, |
flexDirection="column" |
||||
z: nodesWithPosition[0].data.position?.altitude ?? 0 / 1e7, |
gap={majorScale(2)} |
||||
} |
overflow="hidden" |
||||
: { |
> |
||||
y: -35.59, //Longitude
|
<Pane width="100%" height="100%" ref={ref} /> |
||||
x: 148, //Latitude
|
</Pane> |
||||
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