diff --git a/public/configs/simulator/config.json b/public/configs/simulator/config.json new file mode 100644 index 0000000000000000000000000000000000000000..968c6d9f21575ea51be8640086e15dec0c65a065 --- /dev/null +++ b/public/configs/simulator/config.json @@ -0,0 +1,13 @@ +{ + "robots": [ + "./robots/crawler.json" + ], + "ship": { + "rosbridge": { + "uri": "ws://localhost:9090" + }, + "topics": { + "mesh": "/mesh_publisher/shape" + } + } +} diff --git a/public/configs/simulator/robots/crawler.json b/public/configs/simulator/robots/crawler.json new file mode 100644 index 0000000000000000000000000000000000000000..3eb8094e8b0d2f0289a05329270409793c70bf4b --- /dev/null +++ b/public/configs/simulator/robots/crawler.json @@ -0,0 +1,16 @@ +{ + "rosbridge": { + "uri": "ws://localhost:9090" + }, + "type": "SMV", + "name": "Crawler", + "topics": { + "pose": "/mesh_pf1/pose", + "images": [ + ] + }, + "services": { + "prepareMission": "foo", + "executeMission": "foo" + } +} diff --git a/src/App.tsx b/src/App.tsx index 7e84c5bbe4771fe3060ec843da4eeb59546d05e6..cd4be62c71b2a82cea39fb293441e358cfee0f61 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,54 +10,60 @@ import Immersive from './Immersive'; import { useLocation } from 'react-router'; function useQuery() { - const { search } = useLocation(); + const { search } = useLocation(); - return useMemo(() => new URLSearchParams(search), [search]); + return useMemo(() => new URLSearchParams(search), [search]); } function App() { - const addRobot = useStore(state => state.addRobot); - const setShip = useStore(state => state.setShip); - - let isImmersive = useQuery().get('vr') !== null; - - useEffect( - () => { - let cancelled = false; - - const fetchConfig = async () => { - const configResponse = await fetch('https://ship-it-8682bacb.nip.io/config/config.json'); - const json = await configResponse.json(); - validate(json, ConfigSchema, { throwAll: true }); - const config = json as Config; - - if (cancelled) { - return; - } - - setShip(config.ship); - - for (const robot of config.robots) { - const robotResponse = await fetch(`https://ship-it-8682bacb.nip.io/config/${robot}`); - const json = await robotResponse.json(); - validate(json, RobotSchema, { throwAll: true }); - const robotConfig = json as Robot; - if (!cancelled) { - addRobot(robotConfig); - } - } - } - - fetchConfig().catch(console.error); - - return () => { cancelled = true }; - }, - [addRobot, setShip] - ); - - return ( - isImmersive ? <Immersive /> : <Desktop /> - ); + const addRobot = useStore(state => state.addRobot); + const setShip = useStore(state => state.setShip); + const query = useQuery(); + console.log(query); + + let isImmersive = query.get('vr') !== null; + let config = query.get('config'); + + useEffect( + () => { + if (config) { + const configPath = config; + let cancelled = false; + + const fetchConfig = async () => { + const configResponse = await fetch(configPath); + const json = await configResponse.json(); + validate(json, ConfigSchema, { throwAll: true }); + const config = json as Config; + + if (cancelled) { + return; + } + + setShip(config.ship); + + for (const robot of config.robots) { + const robotResponse = await fetch(`${configPath.substring(0, configPath.lastIndexOf('/'))}/${robot}`); + const json = await robotResponse.json(); + validate(json, RobotSchema, { throwAll: true }); + const robotConfig = json as Robot; + if (!cancelled) { + addRobot(robotConfig); + } + } + } + + fetchConfig().catch(console.error); + + return () => { cancelled = true }; + } + }, + [addRobot, setShip, config] + ); + + return ( + isImmersive ? <Immersive /> : <Desktop /> + ); } export default App; diff --git a/src/common/ROS/index.ts b/src/common/ROS/index.ts index 7d3d7b3c8596a1bfc9f85fb782888ae2ef6dc65b..c4fc7acdf37e32fd73008fa1b089c686de8ce2d3 100644 --- a/src/common/ROS/index.ts +++ b/src/common/ROS/index.ts @@ -2,10 +2,12 @@ import std_msgs from './std_msgs'; import geometry_msgs from './geometry_msgs'; import sensor_msgs from './sensor_msgs'; import mesh_msgs from './mesh_msgs'; +import shape_msgs from './shape_msgs'; export type Messages = { std_msgs: std_msgs; geometry_msgs: geometry_msgs; sensor_msgs: sensor_msgs; + shape_msgs: shape_msgs; mesh_msgs: mesh_msgs; } diff --git a/src/common/ROS/shape_msgs.ts b/src/common/ROS/shape_msgs.ts new file mode 100644 index 0000000000000000000000000000000000000000..01d61a15f039fc6bed25cf6a2d63d89726e03125 --- /dev/null +++ b/src/common/ROS/shape_msgs.ts @@ -0,0 +1,14 @@ +import geometry_msgs from "./geometry_msgs"; + +type shape_msgs = { + MeshTriangle: { + vertex_indices: [number, number, number] + } + + Mesh: { + triangles: shape_msgs['MeshTriangle'][] + vertices: geometry_msgs['Point'][] + } +} + +export default shape_msgs; diff --git a/src/common/store.ts b/src/common/store.ts index 7b6d7f8462f0a85628b0a844c3411e3da49200e5..11ec76ccf9dd01b64ef5a3edf7220536b43ddd36 100644 --- a/src/common/store.ts +++ b/src/common/store.ts @@ -1,5 +1,5 @@ -import { useCallback, useEffect } from 'react'; -import { Ros, Topic } from 'roslib'; +import { useCallback, useEffect, useState } from 'react'; +import { Ros, Service, Topic } from 'roslib'; import create from 'zustand'; import { Config } from '../schemas/Config.schema'; import { Robot } from '../schemas/Robot.schema'; @@ -351,3 +351,29 @@ export function useTopic< return connectionTopic.value as Messages[Package][Message] | undefined; } } + +export function useService<Request, Response>(uri?: string, serviceName?: string) { + const connection = useConnection(uri); + + const [service, setService] = useState<Service>(); + + useEffect( + () => { + if (connection?.rosbridge && serviceName) { + setService(new Service({ + ros: connection?.rosbridge, + name: serviceName, + serviceType: '', + })); + return () => setService(undefined); + } + }, + [connection?.rosbridge] + ); + + return (request: Request) => { + return new Promise<Response>((resolve, reject) => { + service?.callService(request, (response) => resolve(response), reject); + }); + }; +} diff --git a/src/features/viewport/Mesh.tsx b/src/features/viewport/Mesh.tsx index efea1404b3eda0c6631a2ecfadc3f0f63c3ffb06..64fce3c055d3cdc32eb7523d6eef9452476329cb 100644 --- a/src/features/viewport/Mesh.tsx +++ b/src/features/viewport/Mesh.tsx @@ -1,6 +1,7 @@ import { EventHandlers } from "@react-three/fiber/dist/declarations/src/core/events"; import { useEffect, useState } from "react"; import { BufferGeometry, DoubleSide, Float32BufferAttribute, Uint32BufferAttribute } from "three"; +import { Messages } from "../../common/ROS"; import { useTopic } from "../../common/store"; export interface MeshProps extends EventHandlers { @@ -16,22 +17,44 @@ const Mesh = (props: MeshProps) => { useEffect(() => { if (mesh) { const geometry = new BufferGeometry(); - const vertices = new Float32Array(mesh.mesh_geometry.vertices.length * 3); - const indices = new Uint32Array(mesh.mesh_geometry.faces.length * 3); - mesh.mesh_geometry.vertices.forEach((vertex, index) => { - vertices[index * 3 + 0] = -vertex.x; - vertices[index * 3 + 2] = vertex.y; - vertices[index * 3 + 1] = vertex.z; - }); - - mesh.mesh_geometry.faces.forEach((face, index) => { - indices[index * 3 + 0] = face.vertex_indices[0]; - indices[index * 3 + 1] = face.vertex_indices[1]; - indices[index * 3 + 2] = face.vertex_indices[2]; - }); - geometry.setIndex(new Uint32BufferAttribute(indices, 1)); - geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3)); + if ('triangles' in mesh && 'vertices' in mesh) { + // Hack to support shape_msgs/Mesh messages + const meshShape = mesh as Messages['shape_msgs']['Mesh']; + const vertices = new Float32Array(meshShape.vertices.length * 3); + const indices = new Uint32Array(meshShape.triangles.length * 3); + + meshShape.vertices.forEach((vertex, index) => { + vertices[index * 3 + 0] = -vertex.x; + vertices[index * 3 + 2] = vertex.y; + vertices[index * 3 + 1] = vertex.z; + }); + + meshShape.triangles.forEach((triangle, index) => { + indices[index * 3 + 0] = triangle.vertex_indices[0]; + indices[index * 3 + 1] = triangle.vertex_indices[1]; + indices[index * 3 + 2] = triangle.vertex_indices[2]; + }); + geometry.setIndex(new Uint32BufferAttribute(indices, 1)); + geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3)); + } else { + const vertices = new Float32Array(mesh.mesh_geometry.vertices.length * 3); + const indices = new Uint32Array(mesh.mesh_geometry.faces.length * 3); + + mesh.mesh_geometry.vertices.forEach((vertex, index) => { + vertices[index * 3 + 0] = -vertex.x; + vertices[index * 3 + 2] = vertex.y; + vertices[index * 3 + 1] = vertex.z; + }); + + mesh.mesh_geometry.faces.forEach((face, index) => { + indices[index * 3 + 0] = face.vertex_indices[0]; + indices[index * 3 + 1] = face.vertex_indices[1]; + indices[index * 3 + 2] = face.vertex_indices[2]; + }); + geometry.setIndex(new Uint32BufferAttribute(indices, 1)); + geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3)); + } geometry.computeVertexNormals(); setGeometry(geometry); if (props.onGeometryUpdate) { diff --git a/src/features/viewport/MissionPlanning.tsx b/src/features/viewport/MissionPlanning.tsx new file mode 100644 index 0000000000000000000000000000000000000000..772ee4e98bc9e41d57a10a2764535fa873a2f56f --- /dev/null +++ b/src/features/viewport/MissionPlanning.tsx @@ -0,0 +1,32 @@ +import { useFrame } from "@react-three/fiber"; +import { useController } from "@react-three/xr"; +import { useEffect, useState } from "react"; +import { Service } from "roslib"; +import { Vector3 } from "three"; +import { useService } from "../../common/store"; + +export interface MissionPlanningProps { +} + +const MissionPlanning = (props: MissionPlanningProps) => { + const rightController = useController('right'); + const [triggerPressed, setTriggerPressed] = useState(false); + + const planMission = useService('', ''); + + useFrame((state, delta, frame) => { + // console.log(rightController?.controller.position); + // + + const triggerCurrentlyPressed = rightController?.inputSource.gamepad?.buttons[0].pressed; + if (!triggerCurrentlyPressed && triggerPressed) { + planMission({ test: 'test' }).then(response => console.log(response)); + // console.log(rightController?.controller.position); + } + setTriggerPressed(triggerCurrentlyPressed || false); + }); + + return null; +} + +export default MissionPlanning;