From aa9d0a54c9b80f8c175ddd1d68d5618fe59b3a6c Mon Sep 17 00:00:00 2001 From: dominikn Date: Fri, 16 Aug 2024 16:03:11 +0200 Subject: [PATCH 01/18] teleop panel stamped --- .../src/panels/Teleop/TeleopPanel.tsx | 88 +++++++++++++------ 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx index c619ebe0a4..80836d0877 100644 --- a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx +++ b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx @@ -36,6 +36,7 @@ const geometryMsgOptions = [ type Config = { topic: undefined | string; publishRate: number; + stamped: boolean; upButton: { field: string; value: number }; downButton: { field: string; value: number }; leftButton: { field: string; value: number }; @@ -53,6 +54,11 @@ function buildSettingsTree(config: Config, topics: readonly Topic[]): SettingsTr value: config.topic, items: topics.map((t) => t.name), }, + stamped: { + label: "Stamped", + input: "boolean", + value: config.stamped, + }, }, children: { upButton: { @@ -123,6 +129,7 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { const { topic, publishRate = 1, + stamped = false, upButton: { field: upField = "linear-x", value: upValue = 1 } = {}, downButton: { field: downField = "linear-x", value: downValue = -1 } = {}, leftButton: { field: leftField = "angular-z", value: leftValue = 1 } = {}, @@ -132,6 +139,7 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { return { topic, publishRate, + stamped, upButton: { field: upField, value: upValue }, downButton: { field: downField, value: downValue }, leftButton: { field: leftField, value: leftValue }, @@ -152,7 +160,7 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { }, []); // setup context render handler and render done handling - const [renderDone, setRenderDone] = useState<() => void>(() => () => {}); + const [renderDone, setRenderDone] = useState<() => void>(() => () => { }); const [colorScheme, setColorScheme] = useState<"dark" | "light">("light"); useLayoutEffect(() => { context.watch("topics"); @@ -177,23 +185,39 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { }, [config, context, saveState, settingsActionHandler, topics]); // advertise topic - const { topic: currentTopic } = config; + const { topic: currentTopic, stamped } = config; useLayoutEffect(() => { if (!currentTopic) { return; } - context.advertise?.(currentTopic, "geometry_msgs/Twist", { - datatypes: new Map([ + const messageType = stamped ? "geometry_msgs/TwistStamped" : "geometry_msgs/Twist"; + const datatypesMap = stamped + ? new Map([ + ["std_msgs/Header", ros1["std_msgs/Header"]], ["geometry_msgs/Vector3", ros1["geometry_msgs/Vector3"]], ["geometry_msgs/Twist", ros1["geometry_msgs/Twist"]], - ]), - }); + ["geometry_msgs/TwistStamped", ros1["geometry_msgs/TwistStamped"]], + ]) + : new Map([ + ["geometry_msgs/Vector3", ros1["geometry_msgs/Vector3"]], + ["geometry_msgs/Twist", ros1["geometry_msgs/Twist"]], + ]); + + context.advertise?.(currentTopic, messageType, { datatypes: datatypesMap }); return () => { context.unadvertise?.(currentTopic); }; - }, [context, currentTopic]); + }, [context, currentTopic, stamped]); + + const getRosTimestamp = () => { + const now = Date.now(); + return { + sec: Math.floor(now / 1000), + nanosec: (now % 1000) * 1e6, + }; + }; useLayoutEffect(() => { if (currentAction == undefined || !currentTopic) { @@ -201,37 +225,43 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { } const message = { - linear: { - x: 0, - y: 0, - z: 0, - }, - angular: { - x: 0, - y: 0, - z: 0, + header: { + stamp: getRosTimestamp(), + frame_id: "base_link", }, + twist: { + linear: { + x: 0, + y: 0, + z: 0, + }, + angular: { + x: 0, + y: 0, + z: 0, + }, + } }; function setFieldValue(field: string, value: number) { switch (field) { case "linear-x": - message.linear.x = value; + message.twist.linear.x = value; break; case "linear-y": - message.linear.y = value; + message.twist.linear.y = value; break; case "linear-z": - message.linear.z = value; + message.twist.linear.z = value; break; case "angular-x": - message.angular.x = value; + message.twist.angular.x = value; break; case "angular-y": - message.angular.y = value; + message.twist.angular.y = value; break; case "angular-z": - message.angular.z = value; + message.twist.angular.z = value; break; } } @@ -258,10 +288,18 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { } const intervalMs = (1000 * 1) / config.publishRate; - context.publish?.(currentTopic, message); - const intervalHandle = setInterval(() => { + + if (config.stamped) { context.publish?.(currentTopic, message); - }, intervalMs); + const intervalHandle = setInterval(() => { + context.publish?.(currentTopic, message); + }, intervalMs); + } else { + context.publish?.(currentTopic, message.twist); + const intervalHandle = setInterval(() => { + context.publish?.(currentTopic, message.twist); + }, intervalMs); + } return () => { clearInterval(intervalHandle); From 3cfe91d8fb04daed9055263218e2ef9be87b6abc Mon Sep 17 00:00:00 2001 From: dominikn Date: Fri, 16 Aug 2024 16:24:06 +0200 Subject: [PATCH 02/18] fix message to send --- .../src/panels/Teleop/TeleopPanel.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx index 80836d0877..d56c6badca 100644 --- a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx +++ b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx @@ -289,17 +289,12 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { const intervalMs = (1000 * 1) / config.publishRate; - if (config.stamped) { - context.publish?.(currentTopic, message); - const intervalHandle = setInterval(() => { - context.publish?.(currentTopic, message); - }, intervalMs); - } else { - context.publish?.(currentTopic, message.twist); - const intervalHandle = setInterval(() => { - context.publish?.(currentTopic, message.twist); - }, intervalMs); - } + const messageToSend = stamped ? message : message.twist; + + context.publish?.(currentTopic, messageToSend); + const intervalHandle = setInterval(() => { + context.publish?.(currentTopic, messageToSend); + }, intervalMs); return () => { clearInterval(intervalHandle); From afdd79af0c4b853d4e80e5872d71584262c9595e Mon Sep 17 00:00:00 2001 From: dominikn Date: Fri, 16 Aug 2024 17:03:10 +0200 Subject: [PATCH 03/18] fix message stamped --- packages/studio-base/src/panels/Teleop/TeleopPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx index d56c6badca..b0d7284333 100644 --- a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx +++ b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx @@ -289,7 +289,7 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { const intervalMs = (1000 * 1) / config.publishRate; - const messageToSend = stamped ? message : message.twist; + const messageToSend = config.stamped ? message : message.twist; context.publish?.(currentTopic, messageToSend); const intervalHandle = setInterval(() => { From 90704f3b4c994d6ca4a3851d5c078c1f3c919ba5 Mon Sep 17 00:00:00 2001 From: dominikn Date: Sat, 17 Aug 2024 18:13:44 +0200 Subject: [PATCH 04/18] created debugging docker based setup for telop stamped messages --- .github/workflows/build-docker-image.yaml | 102 +++++++++++----------- Dockerfile | 14 +-- demo/Dockerfile | 4 + demo/compose.yaml | 20 ++++- demo/just-teleop-layout.json | 30 +++++++ 5 files changed, 111 insertions(+), 59 deletions(-) create mode 100644 demo/Dockerfile create mode 100644 demo/just-teleop-layout.json diff --git a/.github/workflows/build-docker-image.yaml b/.github/workflows/build-docker-image.yaml index 9a1b4fe72d..62cce3e885 100644 --- a/.github/workflows/build-docker-image.yaml +++ b/.github/workflows/build-docker-image.yaml @@ -1,7 +1,7 @@ name: Build a Docker Image on: - push: + # push: workflow_dispatch: jobs: @@ -9,60 +9,60 @@ jobs: runs-on: ubuntu-20.04 steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 - - name: Set FOXGLOVE_VERSION env - run: echo "FOXGLOVE_VERSION=$(yq .version package.json | tr -d '\"')" >> $GITHUB_ENV - shell: bash + - name: Set FOXGLOVE_VERSION env + run: echo "FOXGLOVE_VERSION=$(yq .version package.json | tr -d '\"')" >> $GITHUB_ENV + shell: bash - - name: Set SHORT_DATE env - run: echo "SHORT_DATE=$(date +%Y%m%d)" >> $GITHUB_ENV - shell: bash + - name: Set SHORT_DATE env + run: echo "SHORT_DATE=$(date +%Y%m%d)" >> $GITHUB_ENV + shell: bash - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - version: latest + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: latest - - name: Login to Docker Registry - uses: docker/login-action@v2 - with: - registry: docker.io - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to Docker Registry + uses: docker/login-action@v2 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push (production) - if: github.ref_name == 'main' - uses: docker/build-push-action@v3 - with: - platforms: linux/amd64, linux/arm64 - push: true - file: ./husarion_add_ons/Dockerfile - build-args: | - FOXGLOVE_VERSION=${{ env.FOXGLOVE_VERSION }} - tags: | - husarion/foxglove:${{ env.FOXGLOVE_VERSION }}-${{ env.SHORT_DATE }} - husarion/foxglove:${{ env.FOXGLOVE_VERSION }} - husarion/foxglove:nightly - # cache-from: type=registry,ref=husarnet/dds-router - cache-to: type=inline + - name: Build and push (production) + if: github.ref_name == 'main' + uses: docker/build-push-action@v3 + with: + platforms: linux/amd64, linux/arm64 + push: true + file: ./husarion_add_ons/Dockerfile + build-args: | + FOXGLOVE_VERSION=${{ env.FOXGLOVE_VERSION }} + tags: | + husarion/foxglove:${{ env.FOXGLOVE_VERSION }}-${{ env.SHORT_DATE }} + husarion/foxglove:${{ env.FOXGLOVE_VERSION }} + husarion/foxglove:nightly + # cache-from: type=registry,ref=husarnet/dds-router + cache-to: type=inline - - name: Build and push (feature branch) - if: github.ref_name != 'main' - uses: docker/build-push-action@v3 - with: - # platforms: linux/amd64, linux/arm64 - platforms: linux/amd64 - push: true - file: ./husarion_add_ons/Dockerfile - build-args: | - FOXGLOVE_VERSION=${{ env.FOXGLOVE_VERSION }} - tags: | - husarion/foxglove:${{ github.head_ref || github.ref_name }}-${{ env.FOXGLOVE_VERSION }} - husarion/foxglove:nightly - # cache-from: type=registry,ref=husarnet/dds-router-${{ github.head_ref || github.ref_name }} - cache-to: type=inline + - name: Build and push (feature branch) + if: github.ref_name != 'main' + uses: docker/build-push-action@v3 + with: + # platforms: linux/amd64, linux/arm64 + platforms: linux/amd64 + push: true + file: ./husarion_add_ons/Dockerfile + build-args: | + FOXGLOVE_VERSION=${{ env.FOXGLOVE_VERSION }} + tags: | + husarion/foxglove:${{ github.head_ref || github.ref_name }}-${{ env.FOXGLOVE_VERSION }} + husarion/foxglove:nightly + # cache-from: type=registry,ref=husarnet/dds-router-${{ github.head_ref || github.ref_name }} + cache-to: type=inline diff --git a/Dockerfile b/Dockerfile index db5de890c4..fd246398af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,12 @@ ARG ROS_DISTRO=humble FROM node:16 AS foxglove_build WORKDIR /src -RUN apt-get update && \ - apt-get install -y git-lfs && \ - git clone -b improvements https://github.com/husarion/foxglove-docker . && \ - git lfs pull +# RUN apt-get update && \ +# apt-get install -y git-lfs && \ +# git clone -b improvements https://github.com/husarion/foxglove-docker . && \ +# git lfs pull + +COPY . . RUN corepack enable RUN yarn install --immutable @@ -19,8 +21,8 @@ FROM caddy:2.6.2-alpine WORKDIR /src RUN apk update && apk add \ - bash \ - nss-tools + bash \ + nss-tools COPY --from=foxglove_build /src/web/.webpack ./ diff --git a/demo/Dockerfile b/demo/Dockerfile new file mode 100644 index 0000000000..fa3bca61f1 --- /dev/null +++ b/demo/Dockerfile @@ -0,0 +1,4 @@ +FROM ros:humble-ros-core + +RUN apt update && apt install -y \ + ros-${ROS_DISTRO}-rmw-cyclonedds-cpp diff --git a/demo/compose.yaml b/demo/compose.yaml index 2652b4575b..b1179a6bc4 100644 --- a/demo/compose.yaml +++ b/demo/compose.yaml @@ -7,12 +7,16 @@ x-common-config: &common-config services: foxglove: - image: husarion/foxglove:improvements-nightly + # image: husarion/foxglove:improvements-nightly + build: + context: .. + dockerfile: Dockerfile <<: *common-config ports: - 8080:8080 volumes: - - ./foxglove-layout.json:/foxglove/default-layout.json + # - ./foxglove-layout.json:/foxglove/default-layout.json + - ./just-teleop-layout.json:/foxglove/default-layout.json - ../Caddyfile:/etc/caddy/Caddyfile environment: - DISABLE_CACHE=true @@ -24,3 +28,15 @@ services: ports: - 8765:8765 command: ros2 launch foxglove_bridge foxglove_bridge_launch.xml port:=8765 capabilities:=[clientPublish,parameters,parametersSubscribe,services,connectionGraph,assets] + + ros: + build: + context: . + dockerfile: Dockerfile + container_name: ros-test + <<: *common-config + command: tail -f /dev/null + + # docker exec -it ros-test bash + # source /opt/ros/humble/setup.bash + # ros2 topic echo /cmd_vel geometry_msgs/msg/TwistStamped diff --git a/demo/just-teleop-layout.json b/demo/just-teleop-layout.json new file mode 100644 index 0000000000..d2c95e074d --- /dev/null +++ b/demo/just-teleop-layout.json @@ -0,0 +1,30 @@ +{ + "configById": { + "Teleop!1vd3102": { + "topic": "/cmd_vel", + "publishRate": 5, + "stamped": true, + "upButton": { + "field": "linear-x", + "value": 1 + }, + "downButton": { + "field": "linear-x", + "value": -1 + }, + "leftButton": { + "field": "angular-z", + "value": 1 + }, + "rightButton": { + "field": "angular-z", + "value": -1 + } + } + }, + "globalVariables": { + "": "\"\"" + }, + "userNodes": {}, + "layout": "Teleop!1vd3102" +} \ No newline at end of file From 04c36d532177caaf7539a9ab4a1cd1fba8ebee35 Mon Sep 17 00:00:00 2001 From: dominikn Date: Sat, 17 Aug 2024 19:09:00 +0200 Subject: [PATCH 05/18] in TelopPanel timestamp still is missing something --- demo/compose.yaml | 2 + .../src/panels/Teleop/TeleopPanel.tsx | 56 ++++++++++++------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/demo/compose.yaml b/demo/compose.yaml index b1179a6bc4..457c74825e 100644 --- a/demo/compose.yaml +++ b/demo/compose.yaml @@ -40,3 +40,5 @@ services: # docker exec -it ros-test bash # source /opt/ros/humble/setup.bash # ros2 topic echo /cmd_vel geometry_msgs/msg/TwistStamped + + # docker compose up --build --force-recreate -d diff --git a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx index b0d7284333..3015bc48b6 100644 --- a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx +++ b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx @@ -128,7 +128,7 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { const { topic, - publishRate = 1, + publishRate = 5, stamped = false, upButton: { field: upField = "linear-x", value: upValue = 1 } = {}, downButton: { field: downField = "linear-x", value: downValue = -1 } = {}, @@ -219,29 +219,44 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { }; }; - useLayoutEffect(() => { - if (currentAction == undefined || !currentTopic) { - return; - } - - const message = { + const createMessage = () => { + return { header: { stamp: getRosTimestamp(), frame_id: "base_link", }, twist: { - linear: { - x: 0, - y: 0, - z: 0, - }, - angular: { - x: 0, - y: 0, - z: 0, - }, - } + linear: { x: 0, y: 0, z: 0 }, + angular: { x: 0, y: 0, z: 0 }, + }, }; + }; + + useLayoutEffect(() => { + if (currentAction == undefined || !currentTopic) { + return; + } + + // const message = { + // header: { + // stamp: getRosTimestamp(), + // frame_id: "base_link", + // }, + // twist: { + // linear: { + // x: 0, + // y: 0, + // z: 0, + // }, + // angular: { + // x: 0, + // y: 0, + // z: 0, + // }, + // } + // }; + + const message = createMessage(); function setFieldValue(field: string, value: number) { switch (field) { @@ -289,10 +304,13 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { const intervalMs = (1000 * 1) / config.publishRate; + message.header.stamp = getRosTimestamp(); const messageToSend = config.stamped ? message : message.twist; - context.publish?.(currentTopic, messageToSend); + const intervalHandle = setInterval(() => { + message.header.stamp = getRosTimestamp(); + const messageToSend = config.stamped ? message : message.twist; context.publish?.(currentTopic, messageToSend); }, intervalMs); From ee9b06a2fc24416363397765c93a6b6c56c93ea4 Mon Sep 17 00:00:00 2001 From: dominikn Date: Tue, 27 Aug 2024 19:29:38 +0200 Subject: [PATCH 06/18] fixed joy --- .vscode/settings.json | 11 +- demo/justfile | 8 ++ packages/studio-base/src/panels/Joy/Joy.tsx | 10 ++ .../studio-base/src/panels/Joy/JoyVisual.tsx | 13 +- .../src/panels/Teleop/TeleopPanel.tsx | 119 +++++++----------- 5 files changed, 80 insertions(+), 81 deletions(-) create mode 100644 demo/justfile diff --git a/.vscode/settings.json b/.vscode/settings.json index 3ee747fa13..6a4a78e24f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,7 +26,7 @@ }, "typescript.tsdk": "node_modules/typescript/lib", "typescript.preferences.importModuleSpecifier": "non-relative", - "jest.autoRun": { "watch": false, "onSave": "test-file" }, + // "jest.autoRun": { "watch": false, "onSave": "test-file" }, "jest.jestCommandLine": "yarn test", "eslint.lintTask.enable": true, "[typescriptreact]": { @@ -37,5 +37,12 @@ }, "[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" - } + }, + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + // Disable shell formatting for justfile + // "files.associations": { + // "justfile": "plaintext" // or another file association if there is a custom language for it + // }, } diff --git a/demo/justfile b/demo/justfile new file mode 100644 index 0000000000..4af0c20c1a --- /dev/null +++ b/demo/justfile @@ -0,0 +1,8 @@ +# rebuild: Rebuild the project +rebuild: + #!/bin/bash + docker compose up --build --force-recreate -d + +demo: + #!/bin/bash + docker exec -it ros-test bash -c "source /opt/ros/humble/setup.bash && ros2 topic echo /cmd_vel geometry_msgs/msg/TwistStamped" diff --git a/packages/studio-base/src/panels/Joy/Joy.tsx b/packages/studio-base/src/panels/Joy/Joy.tsx index eb9a9ac964..9ec51b951c 100644 --- a/packages/studio-base/src/panels/Joy/Joy.tsx +++ b/packages/studio-base/src/panels/Joy/Joy.tsx @@ -38,6 +38,7 @@ type Config = { topic: undefined | string; publishRate: number; stamped: boolean; + advanced: boolean; xAxis: Axis; yAxis: Axis; }; @@ -58,6 +59,11 @@ function buildSettingsTree(config: Config, topics: readonly Topic[]): SettingsTr input: "boolean", value: config.stamped, }, + advanced: { + label: "Advanced view", + input: "boolean", + value: config.advanced, + }, }, children: { xAxis: { @@ -119,6 +125,7 @@ function Joy(props: JoyProps): JSX.Element { topic, publishRate = 5, stamped = false, + advanced = false, xAxis: { field: xAxisField = "linear-x", limit: xLimit = 1 } = {}, yAxis: { field: yAxisField = "angular-z", limit: yLimit = 1 } = {}, } = partialConfig; @@ -127,6 +134,7 @@ function Joy(props: JoyProps): JSX.Element { topic, publishRate, stamped, + advanced, xAxis: { field: xAxisField, limit: xLimit }, yAxis: { field: yAxisField, limit: yLimit }, }; @@ -242,6 +250,7 @@ function Joy(props: JoyProps): JSX.Element { setTwistValue(message, config.xAxis, speed.x); setTwistValue(message, config.yAxis, speed.y); context.publish?.(currentTopic, message); + console.log("publishing message joy:", message); }; if (config.publishRate > 0) { @@ -273,6 +282,7 @@ function Joy(props: JoyProps): JSX.Element { {enabled && ( { setVelocity(value); }} diff --git a/packages/studio-base/src/panels/Joy/JoyVisual.tsx b/packages/studio-base/src/panels/Joy/JoyVisual.tsx index 4c1be26814..0999c4080c 100644 --- a/packages/studio-base/src/panels/Joy/JoyVisual.tsx +++ b/packages/studio-base/src/panels/Joy/JoyVisual.tsx @@ -2,7 +2,6 @@ // License, v2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/ -import { Button } from "@mui/material"; import React, { useCallback, useState, useRef } from "react"; import "./styles.css"; @@ -35,6 +34,7 @@ const Arrow = ({ direction, width = 20, height = 7 }: { direction: string; width // Type for the Joystick Props type JoyVisualProps = { disabled?: boolean; + advanced?: boolean; onSpeedChange?: (pos: { x: number; y: number }) => void; xLimit?: number; yLimit?: number; @@ -44,13 +44,13 @@ type JoyVisualProps = { function JoyVisual(props: JoyVisualProps): JSX.Element { const joystickRef = useRef(null); const handleRef = useRef(null); - const { onSpeedChange, disabled = false, xLimit, yLimit } = props; + const { onSpeedChange, disabled = false, advanced = false, xLimit, yLimit } = props; const [speed, setSpeed] = useState<{ x: number; y: number } | undefined>(); const [startPos, setStartPos] = useState<{ x: number; y: number } | undefined>(); const [isDragging, setIsDragging] = useState(false); const [scaleX, setScaleX] = useState(0.5); const [scaleY, setScaleY] = useState(0.5); - const [isEditing, setIsEditing] = useState(false); + // const [isEditing, setIsEditing] = useState(false); const handleStart = useCallback( (event: React.MouseEvent | React.TouchEvent) => { @@ -144,9 +144,6 @@ function JoyVisual(props: JoyVisualProps): JSX.Element { return (
-
@@ -156,11 +153,11 @@ function JoyVisual(props: JoyVisualProps): JSX.Element { - {isEditing && (
+ {advanced && (
({speed?.x.toFixed(2) ?? "0.00"}, {speed?.y.toFixed(2) ?? "0.00"})
)}
- {isEditing && ( + {advanced && (
diff --git a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx index 3015bc48b6..88cf264755 100644 --- a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx +++ b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx @@ -232,91 +232,68 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { }; }; + function setFieldValue(message: any, field: string, value: number) { + switch (field) { + case "linear-x": + message.twist.linear.x = value; + break; + case "linear-y": + message.twist.linear.y = value; + break; + case "linear-z": + message.twist.linear.z = value; + break; + case "angular-x": + message.twist.angular.x = value; + break; + case "angular-y": + message.twist.angular.y = value; + break; + case "angular-z": + message.twist.angular.z = value; + break; + } + } + useLayoutEffect(() => { if (currentAction == undefined || !currentTopic) { return; } - // const message = { - // header: { - // stamp: getRosTimestamp(), - // frame_id: "base_link", - // }, - // twist: { - // linear: { - // x: 0, - // y: 0, - // z: 0, - // }, - // angular: { - // x: 0, - // y: 0, - // z: 0, - // }, - // } - // }; - - const message = createMessage(); - - function setFieldValue(field: string, value: number) { - switch (field) { - case "linear-x": - message.twist.linear.x = value; - break; - case "linear-y": - message.twist.linear.y = value; - break; - case "linear-z": - message.twist.linear.z = value; + const publishMessage = () => { + const message = createMessage(); + + switch (currentAction) { + case DirectionalPadAction.UP: + setFieldValue(message, config.upButton.field, config.upButton.value); break; - case "angular-x": - message.twist.angular.x = value; + case DirectionalPadAction.DOWN: + setFieldValue(message, config.downButton.field, config.downButton.value); break; - case "angular-y": - message.twist.angular.y = value; + case DirectionalPadAction.LEFT: + setFieldValue(message, config.leftButton.field, config.leftButton.value); break; - case "angular-z": - message.twist.angular.z = value; + case DirectionalPadAction.RIGHT: + setFieldValue(message, config.rightButton.field, config.rightButton.value); break; + default: } - } - - switch (currentAction) { - case DirectionalPadAction.UP: - setFieldValue(config.upButton.field, config.upButton.value); - break; - case DirectionalPadAction.DOWN: - setFieldValue(config.downButton.field, config.downButton.value); - break; - case DirectionalPadAction.LEFT: - setFieldValue(config.leftButton.field, config.leftButton.value); - break; - case DirectionalPadAction.RIGHT: - setFieldValue(config.rightButton.field, config.rightButton.value); - break; - default: - } + const messageToSend = config.stamped ? message : message.twist; + context.publish?.(currentTopic, messageToSend); + console.log("publishing message teleop:", messageToSend); + }; // don't publish if rate is 0 or negative - this is a config error on user's part - if (config.publishRate <= 0) { + if (config.publishRate > 0) { + const intervalMs = (1000 * 1) / config.publishRate; + publishMessage(); + const intervalHandle = setInterval(publishMessage, intervalMs); + return () => { + clearInterval(intervalHandle); + }; + } else { return; } - - const intervalMs = (1000 * 1) / config.publishRate; - - message.header.stamp = getRosTimestamp(); - const messageToSend = config.stamped ? message : message.twist; - context.publish?.(currentTopic, messageToSend); - - const intervalHandle = setInterval(() => { - message.header.stamp = getRosTimestamp(); - const messageToSend = config.stamped ? message : message.twist; - context.publish?.(currentTopic, messageToSend); - }, intervalMs); - - return () => { - clearInterval(intervalHandle); - }; }, [context, config, currentTopic, currentAction]); useLayoutEffect(() => { From 45187d777e1b2e62c5a73671771149aaebb84c21 Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Wed, 18 Dec 2024 17:10:13 +0100 Subject: [PATCH 07/18] Clean up --- packages/studio-base/src/panels/Joy/Joy.tsx | 1 - packages/studio-base/src/panels/Joy/JoyVisual.tsx | 4 ++-- packages/studio-base/src/panels/Teleop/TeleopPanel.tsx | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/studio-base/src/panels/Joy/Joy.tsx b/packages/studio-base/src/panels/Joy/Joy.tsx index 9ec51b951c..4b5b5e9c6b 100644 --- a/packages/studio-base/src/panels/Joy/Joy.tsx +++ b/packages/studio-base/src/panels/Joy/Joy.tsx @@ -250,7 +250,6 @@ function Joy(props: JoyProps): JSX.Element { setTwistValue(message, config.xAxis, speed.x); setTwistValue(message, config.yAxis, speed.y); context.publish?.(currentTopic, message); - console.log("publishing message joy:", message); }; if (config.publishRate > 0) { diff --git a/packages/studio-base/src/panels/Joy/JoyVisual.tsx b/packages/studio-base/src/panels/Joy/JoyVisual.tsx index 0999c4080c..3f9af93e75 100644 --- a/packages/studio-base/src/panels/Joy/JoyVisual.tsx +++ b/packages/studio-base/src/panels/Joy/JoyVisual.tsx @@ -102,8 +102,8 @@ function JoyVisual(props: JoyVisualProps): JSX.Element { onSpeedChange?.({ x: v_x, y: v_y }); } - const cx = joyRadius * x_ratio + 50 - const cy = joyRadius * y_ratio + 50 + const cx = joyRadius * x_ratio + 50; + const cy = joyRadius * y_ratio + 50; handleRef.current.setAttribute("cx", cx.toString()); handleRef.current.setAttribute("cy", cy.toString()); diff --git a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx index 88cf264755..67d5d820f4 100644 --- a/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx +++ b/packages/studio-base/src/panels/Teleop/TeleopPanel.tsx @@ -280,7 +280,6 @@ function TeleopPanel(props: TeleopPanelProps): JSX.Element { } const messageToSend = config.stamped ? message : message.twist; context.publish?.(currentTopic, messageToSend); - console.log("publishing message teleop:", messageToSend); }; // don't publish if rate is 0 or negative - this is a config error on user's part From 4395c368f04543ee2b545c85f49e5a3462d8a99c Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Fri, 20 Dec 2024 19:29:53 +0100 Subject: [PATCH 08/18] First prototype of Toggle Button --- packages/studio-base/src/i18n/en/panels.ts | 4 +- .../ToggleSrvButton/ToggleSrvButton.tsx | 230 ++++++++++++++ .../index.stories.tsx | 49 +-- .../index.tsx | 12 +- .../settings.ts | 41 ++- .../styles.css | 0 .../thumbnail.png | 0 .../types.ts | 7 +- .../panels/TriggerButton/TriggerButton.tsx | 291 ------------------ packages/studio-base/src/panels/index.ts | 12 +- 10 files changed, 291 insertions(+), 355 deletions(-) create mode 100644 packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx rename packages/studio-base/src/panels/{TriggerButton => ToggleSrvButton}/index.stories.tsx (68%) rename packages/studio-base/src/panels/{TriggerButton => ToggleSrvButton}/index.tsx (83%) rename packages/studio-base/src/panels/{TriggerButton => ToggleSrvButton}/settings.ts (69%) rename packages/studio-base/src/panels/{TriggerButton => ToggleSrvButton}/styles.css (100%) rename packages/studio-base/src/panels/{TriggerButton => ToggleSrvButton}/thumbnail.png (100%) rename packages/studio-base/src/panels/{TriggerButton => ToggleSrvButton}/types.ts (73%) delete mode 100644 packages/studio-base/src/panels/TriggerButton/TriggerButton.tsx diff --git a/packages/studio-base/src/i18n/en/panels.ts b/packages/studio-base/src/i18n/en/panels.ts index c9765b8d05..40924ebc60 100644 --- a/packages/studio-base/src/i18n/en/panels.ts +++ b/packages/studio-base/src/i18n/en/panels.ts @@ -50,8 +50,8 @@ export const panels = { teleopDescription: "Teleoperate a robot over a live connection.", topicGraph: "Topic Graph", topicGraphDescription: "Display a graph of active nodes, topics, and services.", - triggerButton: "Custom: Trigger Button", - triggerButtonDescription: "Button to call std_srvs/Trigger.", + toggleSrvButton: "Custom: Trigger Srv Button", + toggleSrvButtonDescription: "Button to call services.", userScripts: "User Scripts", userScriptsDescription: "Write custom data transformations in TypeScript. Previously known as Node Playground.", diff --git a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx new file mode 100644 index 0000000000..451dc6df86 --- /dev/null +++ b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx @@ -0,0 +1,230 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import { Button, Palette, Typography } from "@mui/material"; +import * as _ from "lodash-es"; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; +import { makeStyles } from "tss-react/mui"; + +import Log from "@foxglove/log"; +import { PanelExtensionContext, SettingsTreeAction } from "@foxglove/studio"; +import Stack from "@foxglove/studio-base/components/Stack"; +import { Config } from "@foxglove/studio-base/panels/ToggleSrvButton/types"; +import ThemeProvider from "@foxglove/studio-base/theme/ThemeProvider"; + +import { defaultConfig, settingsActionReducer, useSettingsTree } from "./settings"; + +import "./styles.css"; + + +const log = Log.getLogger(__dirname); + +type Props = { + context: PanelExtensionContext; +}; + +type SrvState = { + status: "requesting" | "error" | "success"; + value: string; +}; + +const useStyles = makeStyles<{ state: boolean }>()((theme, { state }) => { + const buttonColor = state ? "#090" : "#900"; + const augmentedButtonColor = theme.palette.augmentColor({ + color: { main: buttonColor }, + }); + + return { + button: { + backgroundColor: augmentedButtonColor.main, + color: augmentedButtonColor.contrastText, + + "&:hover": { + backgroundColor: augmentedButtonColor.dark, + }, + }, + }; +}); + +function parseInput(value: string): { error?: string; parsedObject?: unknown } { + let parsedObject; + let error = undefined; + try { + const parsedAny: unknown = JSON.parse(value); + if (Array.isArray(parsedAny)) { + error = "Request content must be an object, not an array"; + } else if (parsedAny == undefined) { + error = "Request content must be an object, not null"; + } else if (typeof parsedAny !== "object") { + error = `Request content must be an object, not ‘${typeof parsedAny}’`; + } else { + parsedObject = parsedAny; + } + } catch (e) { + error = value.length !== 0 ? e.message : "Enter valid request content as JSON"; + } + return { error, parsedObject }; +} + +// Wrapper component with ThemeProvider so useStyles in the panel receives the right theme. +export function ToggleSrvButton({ context }: Props): JSX.Element { + const [colorScheme, setColorScheme] = useState("light"); + + return ( + + + + ); +} + +function ToggleSrvButtonContent( + props: Props & { setColorScheme: Dispatch> }, +): JSX.Element { + const { context, setColorScheme } = props; + + // panel extensions must notify when they've completed rendering + // onRender will setRenderDone to a done callback which we can invoke after we've rendered + const [renderDone, setRenderDone] = useState<() => void>(() => () => { }); + const [srvState, setSrvState] = useState(); + const [config, setConfig] = useState(() => ({ + ...defaultConfig, + ...(context.initialState as Partial), + })); + const [reqButtonState, setReqButtonState] = useState(config.initialValue); + const { classes } = useStyles({ state: reqButtonState }); + + useEffect(() => { + context.saveState(config); + context.setDefaultPanelTitle( + config.serviceName ? `Call service ${config.serviceName}` : undefined, + ); + }, [config, context]); + + useEffect(() => { + context.watch("colorScheme"); + + context.onRender = (renderSrvState, done) => { + setRenderDone(() => done); + setColorScheme(renderSrvState.colorScheme ?? "light"); + }; + + return () => { + context.onRender = undefined; + }; + }, [context, setColorScheme]); + + useEffect(() => { + setReqButtonState(config.initialValue); + }, [config.initialValue]); + + const { error: requestParseError, parsedObject } = useMemo( + () => parseInput(config.requestPayload ?? ""), + [config.requestPayload], + ); + + const settingsActionHandler = useCallback( + (action: SettingsTreeAction) => { + setConfig((prevConfig) => settingsActionReducer(prevConfig, action)); + }, + [setConfig], + ); + + const settingsTree = useSettingsTree(config); + useEffect(() => { + context.updatePanelSettingsEditor({ + actionHandler: settingsActionHandler, + nodes: settingsTree, + }); + }, [context, settingsActionHandler, settingsTree]); + + const statusMessage = useMemo(() => { + if (context.callService == undefined) { + return "Connect to a data source that supports calling services"; + } + if (!config.serviceName) { + return "Configure a service in the panel settings"; + } + return undefined; + }, [context, config.serviceName]); + + const canToggleSrvButton = Boolean( + context.callService != undefined && + config.requestPayload && + config.serviceName && + parsedObject != undefined && + requestParseError == undefined && + srvState?.status !== "requesting", + ); + + const toggleSrvButtonClicked = useCallback(async () => { + if (!context.callService) { + setSrvState({ status: "error", value: "The data source does not allow calling services" }); + return; + } + + try { + setSrvState({ status: "requesting", value: `Calling ${config.serviceName}...` }); + const requestPayload = JSON.parse("{ \"data\": " + reqButtonState + " }"); + const response = await context.callService( + config.serviceName!, + requestPayload, + ) as { success?: boolean }; + setSrvState({ + status: "success", + value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", + }); + if (response.success === true) { + setReqButtonState(!reqButtonState); + } + } catch (err) { + setSrvState({ status: "error", value: (err as Error).message }); + log.error(err); + } + }, [reqButtonState, context, config.serviceName]); + + // Indicate render is complete - the effect runs after the dom is updated + useEffect(() => { + renderDone(); + }, [renderDone]); + + return ( + + +
+ + {statusMessage && ( + + {statusMessage} + + )} + + + + +
+
+
+ ); +} diff --git a/packages/studio-base/src/panels/TriggerButton/index.stories.tsx b/packages/studio-base/src/panels/ToggleSrvButton/index.stories.tsx similarity index 68% rename from packages/studio-base/src/panels/TriggerButton/index.stories.tsx rename to packages/studio-base/src/panels/ToggleSrvButton/index.stories.tsx index 176e4a5822..d393990947 100644 --- a/packages/studio-base/src/panels/TriggerButton/index.stories.tsx +++ b/packages/studio-base/src/panels/ToggleSrvButton/index.stories.tsx @@ -10,7 +10,7 @@ import { PlayerCapabilities } from "@foxglove/studio-base/players/types"; import PanelSetup, { Fixture } from "@foxglove/studio-base/stories/PanelSetup"; import delay from "@foxglove/studio-base/util/delay"; -import TriggerButtonPanel from "./index"; +import ToggleSrvButtonPanel from "./index"; import { Config } from "./types"; const successResponseJson = JSON.stringify({ success: true }, undefined, 2); @@ -19,8 +19,8 @@ const baseConfig: Config = { requestPayload: `{\n "data": true\n}`, }; -const getFixture = ({ allowTriggerButton }: { allowTriggerButton: boolean }): Fixture => { - const triggerButton = async (service: string, _request: unknown) => { +const getFixture = ({ allowToggleSrvButton }: { allowToggleSrvButton: boolean }): Fixture => { + const toggleSrvButton = async (service: string, _request: unknown) => { if (service !== baseConfig.serviceName) { throw new Error(`Service "${service}" does not exist`); } @@ -35,14 +35,14 @@ const getFixture = ({ allowTriggerButton }: { allowTriggerButton: boolean }): Fi }), ), frame: {}, - capabilities: allowTriggerButton ? [PlayerCapabilities.triggerButtons] : [], - triggerButton, + capabilities: allowToggleSrvButton ? [PlayerCapabilities.toggleSrvButtons] : [], + toggleSrvButton, }; }; export default { - title: "panels/TriggerButton", - component: TriggerButtonPanel, + title: "panels/ToggleSrvButton", + component: ToggleSrvButtonPanel, parameters: { colorScheme: "both-column", }, @@ -59,27 +59,27 @@ export default { export const Default: StoryObj = { render: () => { - return ; + return ; }, }; export const DefaultHorizontalLayout: StoryObj = { render: () => { - return ; + return ; }, }; -export const TriggerButtonEnabled: StoryObj = { +export const ToggleSrvButtonEnabled: StoryObj = { render: () => { - return ; + return ; }, - parameters: { panelSetup: { fixture: getFixture({ allowTriggerButton: true }) } }, + parameters: { panelSetup: { fixture: getFixture({ allowToggleSrvButton: true }) } }, }; -export const TriggerButtonEnabledServiceName: StoryObj = { +export const ToggleSrvButtonEnabledServiceName: StoryObj = { render: () => { - return ; + return ; }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -94,38 +94,39 @@ export const TriggerButtonEnabledServiceName: StoryObj = { } }, - parameters: { panelSetup: { fixture: getFixture({ allowTriggerButton: true }) } }, + parameters: { panelSetup: { fixture: getFixture({ allowToggleSrvButton: true }) } }, }; -export const TriggerButtonEnabledWithCustomButtonSettings: StoryObj = { +export const ToggleSrvButtonEnabledWithCustomButtonSettings: StoryObj = { render: () => { return ( - ); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const buttons = await canvas.findAllByText("Call that funky service"); + const buttons = await canvas.findAllByText("Activate that funky service"); buttons.forEach(async (button) => { await userEvent.hover(button); }); }, - parameters: { panelSetup: { fixture: getFixture({ allowTriggerButton: true }) } }, + parameters: { panelSetup: { fixture: getFixture({ allowToggleSrvButton: true }) } }, }; const validJSON = `{\n "a": 1,\n "b": 2,\n "c": 3\n}`; export const WithValidJSON: StoryObj = { render: () => { - return ; + return ; }, }; @@ -133,14 +134,14 @@ const invalidJSON = `{\n "a": 1,\n 'b: 2,\n "c": 3\n}`; export const WithInvalidJSON: StoryObj = { render: () => { - return ; + return ; }, }; export const CallingServiceThatDoesNotExist: StoryObj = { render: () => { return ( - , context: PanelExtensionContext) { @@ -20,7 +20,7 @@ function initPanel(crash: ReturnType, context: PanelExtensionCo ReactDOM.render( - + , context.panelElement, @@ -36,7 +36,7 @@ type Props = { saveConfig: SaveConfig; }; -function TriggerButtonPanelAdapter(props: Props) { +function ToggleSrvButtonPanelAdapter(props: Props) { const crash = useCrash(); const boundInitPanel = useMemo(() => initPanel.bind(undefined, crash), [crash]); @@ -50,7 +50,7 @@ function TriggerButtonPanelAdapter(props: Props) { ); } -TriggerButtonPanelAdapter.panelType = "TriggerButton"; -TriggerButtonPanelAdapter.defaultConfig = {}; +ToggleSrvButtonPanelAdapter.panelType = "ToggleSrvButton"; +ToggleSrvButtonPanelAdapter.defaultConfig = {}; -export default Panel(TriggerButtonPanelAdapter); +export default Panel(ToggleSrvButtonPanelAdapter); diff --git a/packages/studio-base/src/panels/TriggerButton/settings.ts b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts similarity index 69% rename from packages/studio-base/src/panels/TriggerButton/settings.ts rename to packages/studio-base/src/panels/ToggleSrvButton/settings.ts index 4ef55a60ae..e80a67f012 100644 --- a/packages/studio-base/src/panels/TriggerButton/settings.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts @@ -12,9 +12,10 @@ import { SettingsTreeAction, SettingsTreeNodes } from "@foxglove/studio"; import { Config } from "./types"; export const defaultConfig: Config = { - requestPayload: "{}", - layout: "vertical", - advancedView: true, + initialValue: false, + requestPayload: `{"data": true}`, + buttonActive: "Activate", + buttonDisable: "Deactivate", }; function serviceError(serviceName?: string) { @@ -44,33 +45,29 @@ export function useSettingsTree(config: Config): SettingsTreeNodes { error: serviceError(config.serviceName), value: config.serviceName ?? "", }, - layout: { - label: "Layout", - input: "toggle", - options: [ - { label: "Vertical", value: "vertical" }, - { label: "Horizontal", value: "horizontal" }, - ], - value: config.layout ?? defaultConfig.layout, - }, - advancedView: { - label: "Editing mode", - input: "boolean", - value: config.advancedView, - }, }, }, button: { label: "Button", fields: { - buttonText: { - label: "Title", + initialValue: { + label: "Initial State", + input: "boolean", + value: config.initialValue, + }, + buttonActive: { + label: "Activation Message", + input: "string", + value: config.buttonActive, + placeholder: "Activate", + }, + buttonDisable: { + label: "Deactivation Message", input: "string", - value: config.buttonText, - placeholder: `Call service ${config.serviceName ?? ""}`, + value: config.buttonDisable, + placeholder: "Deactivate", }, buttonTooltip: { label: "Tooltip", input: "string", value: config.buttonTooltip }, - buttonColor: { label: "Color", input: "rgb", value: config.buttonColor }, }, }, }), diff --git a/packages/studio-base/src/panels/TriggerButton/styles.css b/packages/studio-base/src/panels/ToggleSrvButton/styles.css similarity index 100% rename from packages/studio-base/src/panels/TriggerButton/styles.css rename to packages/studio-base/src/panels/ToggleSrvButton/styles.css diff --git a/packages/studio-base/src/panels/TriggerButton/thumbnail.png b/packages/studio-base/src/panels/ToggleSrvButton/thumbnail.png similarity index 100% rename from packages/studio-base/src/panels/TriggerButton/thumbnail.png rename to packages/studio-base/src/panels/ToggleSrvButton/thumbnail.png diff --git a/packages/studio-base/src/panels/TriggerButton/types.ts b/packages/studio-base/src/panels/ToggleSrvButton/types.ts similarity index 73% rename from packages/studio-base/src/panels/TriggerButton/types.ts rename to packages/studio-base/src/panels/ToggleSrvButton/types.ts index cd68532212..56ecda8413 100644 --- a/packages/studio-base/src/panels/TriggerButton/types.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/types.ts @@ -4,10 +4,9 @@ export type Config = { serviceName?: string; + initialValue: boolean; requestPayload?: string; - layout?: "vertical" | "horizontal"; - advancedView: boolean; - buttonText?: string; + buttonActive: string; + buttonDisable: string; buttonTooltip?: string; - buttonColor?: string; }; diff --git a/packages/studio-base/src/panels/TriggerButton/TriggerButton.tsx b/packages/studio-base/src/panels/TriggerButton/TriggerButton.tsx deleted file mode 100644 index 9343580bda..0000000000 --- a/packages/studio-base/src/panels/TriggerButton/TriggerButton.tsx +++ /dev/null @@ -1,291 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/ - -import { Button, Palette, TextField, Tooltip, Typography, inputBaseClasses } from "@mui/material"; -import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; -import { makeStyles } from "tss-react/mui"; - -import Log from "@foxglove/log"; -import { PanelExtensionContext, SettingsTreeAction } from "@foxglove/studio"; -import Stack from "@foxglove/studio-base/components/Stack"; -import { Config } from "@foxglove/studio-base/panels/TriggerButton/types"; -import ThemeProvider from "@foxglove/studio-base/theme/ThemeProvider"; - -import { defaultConfig, settingsActionReducer, useSettingsTree } from "./settings"; - -import "./styles.css"; - - -const log = Log.getLogger(__dirname); - -type Props = { - context: PanelExtensionContext; -}; - -type State = { - status: "requesting" | "error" | "success"; - value: string; -}; - -const useStyles = makeStyles<{ buttonColor?: string }>()((theme, { buttonColor }) => { - const augmentedButtonColor = buttonColor - ? theme.palette.augmentColor({ - color: { main: buttonColor }, - }) - : undefined; - - return { - button: { - backgroundColor: augmentedButtonColor?.main, - color: augmentedButtonColor?.contrastText, - - "&:hover": { - backgroundColor: augmentedButtonColor?.dark, - }, - }, - textarea: { - height: "100%", - - [`.${inputBaseClasses.root}`]: { - backgroundColor: theme.palette.background.paper, - height: "100%", - overflow: "hidden", - padding: theme.spacing(1, 0.5), - textAlign: "left", - width: "100%", - - [`.${inputBaseClasses.input}`]: { - height: "100% !important", - lineHeight: 1.4, - fontFamily: theme.typography.fontMonospace, - overflow: "auto !important", - resize: "none", - }, - }, - }, - }; -}); - -function parseInput(value: string): { error?: string; parsedObject?: unknown } { - let parsedObject; - let error = undefined; - try { - const parsedAny: unknown = JSON.parse(value); - if (Array.isArray(parsedAny)) { - error = "Request content must be an object, not an array"; - } else if (parsedAny == undefined) { - error = "Request content must be an object, not null"; - } else if (typeof parsedAny !== "object") { - error = `Request content must be an object, not ‘${typeof parsedAny}’`; - } else { - parsedObject = parsedAny; - } - } catch (e) { - error = value.length !== 0 ? e.message : "Enter valid request content as JSON"; - } - return { error, parsedObject }; -} - -// Wrapper component with ThemeProvider so useStyles in the panel receives the right theme. -export function TriggerButton({ context }: Props): JSX.Element { - const [colorScheme, setColorScheme] = useState("light"); - - return ( - - - - ); -} - -function TriggerButtonContent( - props: Props & { setColorScheme: Dispatch> }, -): JSX.Element { - const { context, setColorScheme } = props; - - // panel extensions must notify when they've completed rendering - // onRender will setRenderDone to a done callback which we can invoke after we've rendered - const [renderDone, setRenderDone] = useState<() => void>(() => () => { }); - const [state, setState] = useState(); - const [config, setConfig] = useState(() => ({ - ...defaultConfig, - ...(context.initialState as Partial), - })); - const { classes } = useStyles({ buttonColor: config.buttonColor }); - - useEffect(() => { - context.saveState(config); - context.setDefaultPanelTitle( - config.serviceName ? `Call service ${config.serviceName}` : undefined, - ); - }, [config, context]); - - useEffect(() => { - context.watch("colorScheme"); - - context.onRender = (renderState, done) => { - setRenderDone(() => done); - setColorScheme(renderState.colorScheme ?? "light"); - }; - - return () => { - context.onRender = undefined; - }; - }, [context, setColorScheme]); - - const { error: requestParseError, parsedObject } = useMemo( - () => parseInput(config.requestPayload ?? ""), - [config.requestPayload], - ); - - const settingsActionHandler = useCallback( - (action: SettingsTreeAction) => { - setConfig((prevConfig) => settingsActionReducer(prevConfig, action)); - }, - [setConfig], - ); - - const settingsTree = useSettingsTree(config); - useEffect(() => { - context.updatePanelSettingsEditor({ - actionHandler: settingsActionHandler, - nodes: settingsTree, - }); - }, [context, settingsActionHandler, settingsTree]); - - const statusMessage = useMemo(() => { - if (context.callService == undefined) { - return "Connect to a data source that supports calling services"; - } - if (!config.serviceName) { - return "Configure a service in the panel settings"; - } - return undefined; - }, [context, config.serviceName]); - - const canTriggerButton = Boolean( - context.callService != undefined && - config.requestPayload && - config.serviceName && - parsedObject != undefined && - state?.status !== "requesting", - ); - - const triggerButtonClicked = useCallback(async () => { - if (!context.callService) { - setState({ status: "error", value: "The data source does not allow calling services" }); - return; - } - - try { - setState({ status: "requesting", value: `Calling ${config.serviceName}...` }); - const response = await context.callService( - config.serviceName!, - JSON.parse(config.requestPayload!), - ); - setState({ - status: "success", - value: - JSON.stringify( - response, - // handle stringify BigInt correctly - (_key, value) => (typeof value === "bigint" ? value.toString() : value), - 2, - ) ?? "", - }); - } catch (err) { - setState({ status: "error", value: (err as Error).message }); - log.error(err); - } - }, [context, config.serviceName, config.requestPayload]); - - // Indicate render is complete - the effect runs after the dom is updated - useEffect(() => { - renderDone(); - }, [renderDone]); - - return ( - - {config.advancedView && ( - - - - Request - - { - setConfig({ ...config, requestPayload: event.target.value }); - }} - error={requestParseError != undefined} - /> - {requestParseError && ( - - {requestParseError} - - )} - - - - Response - - - - - )} - {!config.advancedView && ( - - -
- - {statusMessage && ( - - {statusMessage} - - )} - - - - - - -
-
- )} -
- ); -} diff --git a/packages/studio-base/src/panels/index.ts b/packages/studio-base/src/panels/index.ts index 2a83644f7b..123f3f48f8 100644 --- a/packages/studio-base/src/panels/index.ts +++ b/packages/studio-base/src/panels/index.ts @@ -26,7 +26,7 @@ import tableThumbnail from "./Table/thumbnail.png"; import teleopThumbnail from "./Teleop/thumbnail.png"; import threeDeeRenderThumbnail from "./ThreeDeeRender/thumbnail.png"; import topicGraphThumbnail from "./TopicGraph/thumbnail.png"; -import triggerButtonThumbnail from "./TriggerButton/thumbnail.png"; +import toggleSrvButtonThumbnail from "./ToggleSrvButton/thumbnail.png"; import variableSliderThumbnail from "./VariableSlider/thumbnail.png"; import diagnosticStatusThumbnail from "./diagnostics/thumbnails/diagnostic-status.png"; import diagnosticSummaryThumbnail from "./diagnostics/thumbnails/diagnostic-summary.png"; @@ -199,11 +199,11 @@ export const getBuiltin: (t: TFunction<"panels">) => PanelInfo[] = (t) => [ hasCustomToolbar: true, }, { - title: t("triggerButton"), - type: "TriggerButton", - description: t("triggerButtonDescription"), - thumbnail: triggerButtonThumbnail, - module: async () => await import("./TriggerButton"), + title: t("toggleSrvButton"), + type: "ToggleSrvButton", + description: t("toggleSrvButtonDescription"), + thumbnail: toggleSrvButtonThumbnail, + module: async () => await import("./ToggleSrvButton"), }, { title: t("eStop"), From 1db5f9c2584bc0095cd18a47b6c70053ae3db135 Mon Sep 17 00:00:00 2001 From: "rafal.gorecki" Date: Mon, 23 Dec 2024 18:08:07 +0100 Subject: [PATCH 09/18] Add status to ToggleButton --- .../studio-base/src/panels/EStop/EStop.tsx | 32 +--- .../src/panels/EStop/index.stories.tsx | 37 ++-- .../studio-base/src/panels/EStop/settings.ts | 1 - .../studio-base/src/panels/EStop/types.ts | 1 - .../ToggleSrvButton/ToggleSrvButton.tsx | 180 ++++++++++++++---- .../src/panels/ToggleSrvButton/settings.ts | 14 +- .../src/panels/ToggleSrvButton/types.ts | 5 +- 7 files changed, 162 insertions(+), 108 deletions(-) diff --git a/packages/studio-base/src/panels/EStop/EStop.tsx b/packages/studio-base/src/panels/EStop/EStop.tsx index 46db6c8a8e..336bd1d933 100644 --- a/packages/studio-base/src/panels/EStop/EStop.tsx +++ b/packages/studio-base/src/panels/EStop/EStop.tsx @@ -65,26 +65,6 @@ const useStyles = makeStyles<{ state?: string }>()((theme, { state }) => { }; }); -function parseInput(value: string): { error?: string; parsedObject?: unknown } { - let parsedObject; - let error = undefined; - try { - const parsedAny: unknown = JSON.parse(value); - if (Array.isArray(parsedAny)) { - error = "Request content must be an object, not an array"; - } else if (parsedAny == undefined) { - error = "Request content must be an object, not null"; - } else if (typeof parsedAny !== "object") { - error = `Request content must be an object, not ‘${typeof parsedAny}’`; - } else { - parsedObject = parsedAny; - } - } catch (e) { - error = value.length !== 0 ? e.message : "Enter valid request content as JSON"; - } - return { error, parsedObject }; -} - function getSingleDataItem(results: unknown[]) { if (results.length <= 1) { return results[0]; @@ -253,11 +233,6 @@ function EStopContent( }; }, [context, state.parsedPath?.topicName]); - const { error: requestParseError, parsedObject } = useMemo( - () => parseInput(config.requestPayload ?? ""), - [config.requestPayload], - ); - const settingsActionHandler = useCallback( (action: SettingsTreeAction) => { setConfig((prevConfig) => settingsActionReducer(prevConfig, action)); @@ -285,12 +260,9 @@ function EStopContent( const canEStop = Boolean( context.callService != undefined && - config.requestPayload && config.goServiceName && config.stopServiceName && eStopAction != undefined && - parsedObject != undefined && - requestParseError == undefined && reqState?.status !== "requesting", ); @@ -309,7 +281,7 @@ function EStopContent( try { setReqState({ status: "requesting", value: `Calling ${serviceName}...` }); - const response = await context.callService(serviceName, JSON.parse(config.requestPayload!)); + const response = await context.callService(serviceName, {}); setReqState({ status: "success", value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", @@ -319,7 +291,7 @@ function EStopContent( setReqState({ status: "error", value: (err as Error).message }); log.error(err); } - }, [context, eStopAction, config.goServiceName, config.stopServiceName, config.requestPayload]); + }, [context, eStopAction, config.goServiceName, config.stopServiceName]); // Setting eStopAction based on state.latestMatchingQueriedData useEffect(() => { diff --git a/packages/studio-base/src/panels/EStop/index.stories.tsx b/packages/studio-base/src/panels/EStop/index.stories.tsx index 699dd46777..4f62703f5c 100644 --- a/packages/studio-base/src/panels/EStop/index.stories.tsx +++ b/packages/studio-base/src/panels/EStop/index.stories.tsx @@ -15,12 +15,13 @@ import { Config } from "./types"; const successResponseJson = JSON.stringify({ success: true }, undefined, 2); const baseConfig: Config = { - goServiceName: "/set_bool", - requestPayload: `{\n "data": true\n}`, + goServiceName: "/trigger", + stopServiceName: "/reset", + statusTopicName: "/status", }; -const getFixture = ({ allowEStop }: { allowEStop: boolean }): Fixture => { - const eStop = async (service: string, _request: unknown) => { +const getFixture = ({ allowCallService }: { allowCallService: boolean }): Fixture => { + const callService = async (service: string, _request: unknown) => { if (service !== baseConfig.goServiceName) { throw new Error(`Service "${service}" does not exist`); } @@ -35,8 +36,8 @@ const getFixture = ({ allowEStop }: { allowEStop: boolean }): Fixture => { }), ), frame: {}, - capabilities: allowEStop ? [PlayerCapabilities.eStops] : [], - eStop, + capabilities: allowCallService ? [PlayerCapabilities.callServices] : [], + callService, }; }; @@ -68,7 +69,7 @@ export const EStopEnabled: StoryObj = { return ; }, - parameters: { panelSetup: { fixture: getFixture({ allowEStop: true }) } }, + parameters: { panelSetup: { fixture: getFixture({ allowCallService: true }) } }, }; export const EStopEnabledServiceName: StoryObj = { @@ -88,7 +89,7 @@ export const EStopEnabledServiceName: StoryObj = { } }, - parameters: { panelSetup: { fixture: getFixture({ allowEStop: true }) } }, + parameters: { panelSetup: { fixture: getFixture({ allowCallService: true }) } }, }; export const EStopEnabledWithCustomButtonSettings: StoryObj = { @@ -107,23 +108,7 @@ export const EStopEnabledWithCustomButtonSettings: StoryObj = { }); }, - parameters: { panelSetup: { fixture: getFixture({ allowEStop: true }) } }, -}; - -const validJSON = `{\n "a": 1,\n "b": 2,\n "c": 3\n}`; - -export const WithValidJSON: StoryObj = { - render: () => { - return ; - }, -}; - -const invalidJSON = `{\n "a": 1,\n 'b: 2,\n "c": 3\n}`; - -export const WithInvalidJSON: StoryObj = { - render: () => { - return ; - }, + parameters: { panelSetup: { fixture: getFixture({ allowCallService: true }) } }, }; export const CallingServiceThatDoesNotExist: StoryObj = { @@ -150,5 +135,5 @@ export const CallingServiceThatDoesNotExist: StoryObj = { } }, - parameters: { panelSetup: { fixture: getFixture({ allowEStop: true }) } }, + parameters: { panelSetup: { fixture: getFixture({ allowCallService: true }) } }, }; diff --git a/packages/studio-base/src/panels/EStop/settings.ts b/packages/studio-base/src/panels/EStop/settings.ts index 23aaca8ad3..b87b0c189a 100644 --- a/packages/studio-base/src/panels/EStop/settings.ts +++ b/packages/studio-base/src/panels/EStop/settings.ts @@ -12,7 +12,6 @@ import { SettingsTreeAction, SettingsTreeNodes } from "@foxglove/studio"; import { Config } from "./types"; export const defaultConfig: Config = { - requestPayload: "{}", goServiceName: "", stopServiceName: "", statusTopicName: "", diff --git a/packages/studio-base/src/panels/EStop/types.ts b/packages/studio-base/src/panels/EStop/types.ts index 34cd0673bb..6cd5e5e6cb 100644 --- a/packages/studio-base/src/panels/EStop/types.ts +++ b/packages/studio-base/src/panels/EStop/types.ts @@ -6,5 +6,4 @@ export type Config = { goServiceName: string; stopServiceName: string; statusTopicName: string; - requestPayload?: string; }; diff --git a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx index 451dc6df86..dd11c42e0c 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx +++ b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx @@ -4,11 +4,13 @@ import { Button, Palette, Typography } from "@mui/material"; import * as _ from "lodash-es"; -import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; +import { Dispatch, SetStateAction, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useState } from "react"; import { makeStyles } from "tss-react/mui"; import Log from "@foxglove/log"; -import { PanelExtensionContext, SettingsTreeAction } from "@foxglove/studio"; +import { parseMessagePath, MessagePath } from "@foxglove/message-path"; +import { MessageEvent, PanelExtensionContext, SettingsTreeAction } from "@foxglove/studio"; +import { simpleGetMessagePathDataItems } from "@foxglove/studio-base/components/MessagePathSyntax/simpleGetMessagePathDataItems"; import Stack from "@foxglove/studio-base/components/Stack"; import { Config } from "@foxglove/studio-base/panels/ToggleSrvButton/types"; import ThemeProvider from "@foxglove/studio-base/theme/ThemeProvider"; @@ -29,7 +31,21 @@ type SrvState = { value: string; }; -const useStyles = makeStyles<{ state: boolean }>()((theme, { state }) => { +type State = { + path: string; + parsedPath: MessagePath | undefined; + latestMessage: MessageEvent | undefined; + latestMatchingQueriedData: unknown; + error: Error | undefined; + pathParseError: string | undefined; +}; + +type Action = + | { type: "frame"; messages: readonly MessageEvent[] } + | { type: "path"; path: string } + | { type: "seek" }; + +const useStyles = makeStyles<{ state?: boolean }>()((theme, { state }) => { const buttonColor = state ? "#090" : "#900"; const augmentedButtonColor = theme.palette.augmentColor({ color: { main: buttonColor }, @@ -47,24 +63,81 @@ const useStyles = makeStyles<{ state: boolean }>()((theme, { state }) => { }; }); -function parseInput(value: string): { error?: string; parsedObject?: unknown } { - let parsedObject; - let error = undefined; +function getSingleDataItem(results: unknown[]) { + if (results.length <= 1) { + return results[0]; + } + throw new Error("Message path produced multiple results"); +} + +function reducer(state: State, action: Action): State { try { - const parsedAny: unknown = JSON.parse(value); - if (Array.isArray(parsedAny)) { - error = "Request content must be an object, not an array"; - } else if (parsedAny == undefined) { - error = "Request content must be an object, not null"; - } else if (typeof parsedAny !== "object") { - error = `Request content must be an object, not ‘${typeof parsedAny}’`; - } else { - parsedObject = parsedAny; + switch (action.type) { + case "frame": { + if (state.pathParseError != undefined) { + return { ...state, latestMessage: _.last(action.messages), error: undefined }; + } + let latestMatchingQueriedData = state.latestMatchingQueriedData; + let latestMessage = state.latestMessage; + if (state.parsedPath) { + for (const message of action.messages) { + if (message.topic !== state.parsedPath.topicName) { + continue; + } + const data = getSingleDataItem( + simpleGetMessagePathDataItems(message, state.parsedPath), + ); + if (data != undefined) { + latestMatchingQueriedData = data; + latestMessage = message; + } + } + } + return { ...state, latestMessage, latestMatchingQueriedData, error: undefined }; + } + case "path": { + const newPath = parseMessagePath(action.path); + let pathParseError: string | undefined; + if ( + newPath?.messagePath.some( + (part) => + (part.type === "filter" && typeof part.value === "object") || + (part.type === "slice" && + (typeof part.start === "object" || typeof part.end === "object")), + ) === true + ) { + pathParseError = "Message paths using variables are not currently supported"; + } + let latestMatchingQueriedData: unknown; + let error: Error | undefined; + try { + latestMatchingQueriedData = + newPath && pathParseError == undefined && state.latestMessage + ? getSingleDataItem(simpleGetMessagePathDataItems(state.latestMessage, newPath)) + : undefined; + } catch (err) { + error = err; + } + return { + ...state, + path: action.path, + parsedPath: newPath, + latestMatchingQueriedData, + error, + pathParseError, + }; + } + case "seek": + return { + ...state, + latestMessage: undefined, + latestMatchingQueriedData: undefined, + error: undefined, + }; } - } catch (e) { - error = value.length !== 0 ? e.message : "Enter valid request content as JSON"; + } catch (error) { + return { ...state, latestMatchingQueriedData: undefined, error }; } - return { error, parsedObject }; } // Wrapper component with ThemeProvider so useStyles in the panel receives the right theme. @@ -91,8 +164,33 @@ function ToggleSrvButtonContent( ...defaultConfig, ...(context.initialState as Partial), })); - const [reqButtonState, setReqButtonState] = useState(config.initialValue); - const { classes } = useStyles({ state: reqButtonState }); + const [buttonState, setButtonState] = useState(); + + const { classes } = useStyles({ state: buttonState }); + + const [state, dispatch] = useReducer( + reducer, + { ...config, path: config.stateFieldName }, + ({ path }): State => ({ + path: path ?? "", + parsedPath: parseMessagePath(path), + latestMessage: undefined, + latestMatchingQueriedData: undefined, + pathParseError: undefined, + error: undefined, + }), + ); + + useLayoutEffect(() => { + dispatch({ type: "path", path: config.stateFieldName }); + }, [config.stateFieldName]); + + useEffect(() => { + context.saveState(config); + context.setDefaultPanelTitle( + config.serviceName ? `Unspecified` : undefined, + ); + }, [config, context]); useEffect(() => { context.saveState(config); @@ -115,13 +213,13 @@ function ToggleSrvButtonContent( }, [context, setColorScheme]); useEffect(() => { - setReqButtonState(config.initialValue); - }, [config.initialValue]); - - const { error: requestParseError, parsedObject } = useMemo( - () => parseInput(config.requestPayload ?? ""), - [config.requestPayload], - ); + if (state.parsedPath?.topicName != undefined) { + context.subscribe([{ topic: state.parsedPath.topicName, preload: false }]); + } + return () => { + context.unsubscribeAll(); + }; + }, [context, state.parsedPath?.topicName]); const settingsActionHandler = useCallback( (action: SettingsTreeAction) => { @@ -150,10 +248,9 @@ function ToggleSrvButtonContent( const canToggleSrvButton = Boolean( context.callService != undefined && - config.requestPayload && config.serviceName && - parsedObject != undefined && - requestParseError == undefined && + config.stateFieldName && + buttonState != undefined && srvState?.status !== "requesting", ); @@ -165,23 +262,26 @@ function ToggleSrvButtonContent( try { setSrvState({ status: "requesting", value: `Calling ${config.serviceName}...` }); - const requestPayload = JSON.parse("{ \"data\": " + reqButtonState + " }"); - const response = await context.callService( - config.serviceName!, - requestPayload, - ) as { success?: boolean }; + const requestPayload = { data: !buttonState }; + const response = await context.callService(config.serviceName!, requestPayload) as { success?: boolean }; setSrvState({ status: "success", value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", }); - if (response.success === true) { - setReqButtonState(!reqButtonState); - } + setButtonState(undefined); } catch (err) { setSrvState({ status: "error", value: (err as Error).message }); log.error(err); } - }, [reqButtonState, context, config.serviceName]); + }, [context, buttonState, config.serviceName]); + + // Setting buttonState based on state.latestMatchingQueriedData + useEffect(() => { + if (state.latestMatchingQueriedData != undefined) { + const data = state.latestMatchingQueriedData as boolean; + setButtonState(data); + } + }, [state.latestMatchingQueriedData]); // Indicate render is complete - the effect runs after the dom is updated useEffect(() => { @@ -219,7 +319,7 @@ function ToggleSrvButtonContent( borderRadius: "0.3rem", }} > - {reqButtonState ? config.buttonActive : config.buttonDisable} + {buttonState ? config.buttonActive : config.buttonDisable} diff --git a/packages/studio-base/src/panels/ToggleSrvButton/settings.ts b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts index e80a67f012..f2715b1525 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/settings.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts @@ -12,8 +12,8 @@ import { SettingsTreeAction, SettingsTreeNodes } from "@foxglove/studio"; import { Config } from "./types"; export const defaultConfig: Config = { - initialValue: false, - requestPayload: `{"data": true}`, + serviceName: "", + stateFieldName: "", buttonActive: "Activate", buttonDisable: "Deactivate", }; @@ -45,16 +45,16 @@ export function useSettingsTree(config: Config): SettingsTreeNodes { error: serviceError(config.serviceName), value: config.serviceName ?? "", }, + stateFieldName: { + label: "Topic State Field Name", + input: "string", + value: config.stateFieldName, + }, }, }, button: { label: "Button", fields: { - initialValue: { - label: "Initial State", - input: "boolean", - value: config.initialValue, - }, buttonActive: { label: "Activation Message", input: "string", diff --git a/packages/studio-base/src/panels/ToggleSrvButton/types.ts b/packages/studio-base/src/panels/ToggleSrvButton/types.ts index 56ecda8413..37d3b7024c 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/types.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/types.ts @@ -3,9 +3,8 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ export type Config = { - serviceName?: string; - initialValue: boolean; - requestPayload?: string; + serviceName: string; + stateFieldName: string; buttonActive: string; buttonDisable: string; buttonTooltip?: string; From 0a3c64f2c4dcfc7a0365e21e971d6ba83dd68de0 Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Fri, 27 Dec 2024 14:32:49 +0100 Subject: [PATCH 10/18] Fix subscription --- .../studio-base/src/panels/EStop/EStop.tsx | 4 +- .../studio-base/src/panels/EStop/settings.ts | 12 +++- .../ToggleSrvButton/ToggleSrvButton.tsx | 64 ++++++++++--------- .../panels/ToggleSrvButton/index.stories.tsx | 17 +---- .../src/panels/ToggleSrvButton/settings.ts | 46 +++++++++---- .../src/panels/ToggleSrvButton/types.ts | 9 +-- 6 files changed, 85 insertions(+), 67 deletions(-) diff --git a/packages/studio-base/src/panels/EStop/EStop.tsx b/packages/studio-base/src/panels/EStop/EStop.tsx index 336bd1d933..82e08cb24b 100644 --- a/packages/studio-base/src/panels/EStop/EStop.tsx +++ b/packages/studio-base/src/panels/EStop/EStop.tsx @@ -240,7 +240,7 @@ function EStopContent( [setConfig], ); - const settingsTree = useSettingsTree(config); + const settingsTree = useSettingsTree(config, state.pathParseError); useEffect(() => { context.updatePanelSettingsEditor({ actionHandler: settingsActionHandler, @@ -281,12 +281,12 @@ function EStopContent( try { setReqState({ status: "requesting", value: `Calling ${serviceName}...` }); + setEStopAction(undefined); const response = await context.callService(serviceName, {}); setReqState({ status: "success", value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", }); - setEStopAction(undefined); } catch (err) { setReqState({ status: "error", value: (err as Error).message }); log.error(err); diff --git a/packages/studio-base/src/panels/EStop/settings.ts b/packages/studio-base/src/panels/EStop/settings.ts index b87b0c189a..d1982a3c63 100644 --- a/packages/studio-base/src/panels/EStop/settings.ts +++ b/packages/studio-base/src/panels/EStop/settings.ts @@ -33,7 +33,12 @@ export function settingsActionReducer(prevConfig: Config, action: SettingsTreeAc }); } -export function useSettingsTree(config: Config): SettingsTreeNodes { +const supportedDataTypes = ["bool"]; + +export function useSettingsTree( + config: Config, + pathParseError: string | undefined, +): SettingsTreeNodes { const settings = useMemo( (): SettingsTreeNodes => ({ general: { @@ -52,9 +57,10 @@ export function useSettingsTree(config: Config): SettingsTreeNodes { }, statusTopicName: { label: "EStop status topic", - input: "string", - error: serviceError(config.statusTopicName), + input: "messagepath", value: config.statusTopicName, + error: pathParseError, + validTypes: supportedDataTypes, }, }, }, diff --git a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx index dd11c42e0c..e24ce904cf 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx +++ b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx @@ -45,8 +45,8 @@ type Action = | { type: "path"; path: string } | { type: "seek" }; -const useStyles = makeStyles<{ state?: boolean }>()((theme, { state }) => { - const buttonColor = state ? "#090" : "#900"; +const useStyles = makeStyles<{ action?: boolean; config: Config }>()((theme, { action, config }) => { + const buttonColor = action === true ? config.activationColor : config.deactivationColor; const augmentedButtonColor = theme.palette.augmentColor({ color: { main: buttonColor }, }); @@ -140,7 +140,6 @@ function reducer(state: State, action: Action): State { } } -// Wrapper component with ThemeProvider so useStyles in the panel receives the right theme. export function ToggleSrvButton({ context }: Props): JSX.Element { const [colorScheme, setColorScheme] = useState("light"); @@ -155,24 +154,20 @@ function ToggleSrvButtonContent( props: Props & { setColorScheme: Dispatch> }, ): JSX.Element { const { context, setColorScheme } = props; - - // panel extensions must notify when they've completed rendering - // onRender will setRenderDone to a done callback which we can invoke after we've rendered const [renderDone, setRenderDone] = useState<() => void>(() => () => { }); const [srvState, setSrvState] = useState(); - const [config, setConfig] = useState(() => ({ + const [config, setConfig] = useState(() => ({ ...defaultConfig, ...(context.initialState as Partial), })); - const [buttonState, setButtonState] = useState(); - - const { classes } = useStyles({ state: buttonState }); + const [buttonAction, setButtonAction] = useState(undefined); + const { classes } = useStyles({ action: buttonAction, config }); const [state, dispatch] = useReducer( reducer, - { ...config, path: config.stateFieldName }, + { ...config, path: config.statusTopicName }, ({ path }): State => ({ - path: path ?? "", + path, parsedPath: parseMessagePath(path), latestMessage: undefined, latestMatchingQueriedData: undefined, @@ -182,14 +177,13 @@ function ToggleSrvButtonContent( ); useLayoutEffect(() => { - dispatch({ type: "path", path: config.stateFieldName }); - }, [config.stateFieldName]); + dispatch({ type: "path", path: config.statusTopicName }); + }, [config.statusTopicName]); useEffect(() => { context.saveState(config); context.setDefaultPanelTitle( - config.serviceName ? `Unspecified` : undefined, - ); + config.statusTopicName === "" ? undefined : config.statusTopicName); }, [config, context]); useEffect(() => { @@ -202,10 +196,20 @@ function ToggleSrvButtonContent( useEffect(() => { context.watch("colorScheme"); - context.onRender = (renderSrvState, done) => { + context.onRender = (renderState, done) => { setRenderDone(() => done); - setColorScheme(renderSrvState.colorScheme ?? "light"); + setColorScheme(renderState.colorScheme ?? "light"); + + if (renderState.didSeek === true) { + dispatch({ type: "seek" }); + } + + if (renderState.currentFrame) { + dispatch({ type: "frame", messages: renderState.currentFrame }); + } }; + context.watch("currentFrame"); + context.watch("didSeek"); return () => { context.onRender = undefined; @@ -228,7 +232,7 @@ function ToggleSrvButtonContent( [setConfig], ); - const settingsTree = useSettingsTree(config); + const settingsTree = useSettingsTree(config, state.pathParseError); useEffect(() => { context.updatePanelSettingsEditor({ actionHandler: settingsActionHandler, @@ -249,8 +253,8 @@ function ToggleSrvButtonContent( const canToggleSrvButton = Boolean( context.callService != undefined && config.serviceName && - config.stateFieldName && - buttonState != undefined && + config.statusTopicName && + buttonAction != undefined && srvState?.status !== "requesting", ); @@ -262,24 +266,24 @@ function ToggleSrvButtonContent( try { setSrvState({ status: "requesting", value: `Calling ${config.serviceName}...` }); - const requestPayload = { data: !buttonState }; - const response = await context.callService(config.serviceName!, requestPayload) as { success?: boolean }; + const requestPayload = { data: buttonAction ?? true }; + setButtonAction(undefined); + const response = await context.callService(config.serviceName, requestPayload) as { success?: boolean }; setSrvState({ status: "success", value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", }); - setButtonState(undefined); } catch (err) { setSrvState({ status: "error", value: (err as Error).message }); log.error(err); } - }, [context, buttonState, config.serviceName]); + }, [context, buttonAction, config.serviceName]); - // Setting buttonState based on state.latestMatchingQueriedData + // Setting buttonAction based on state.latestMatchingQueriedData useEffect(() => { - if (state.latestMatchingQueriedData != undefined) { - const data = state.latestMatchingQueriedData as boolean; - setButtonState(data); + const data = state.latestMatchingQueriedData; + if (typeof data === "boolean") { + setButtonAction(!data); } }, [state.latestMatchingQueriedData]); @@ -319,7 +323,7 @@ function ToggleSrvButtonContent( borderRadius: "0.3rem", }} > - {buttonState ? config.buttonActive : config.buttonDisable} + {buttonAction === true ? config.activationText : buttonAction === false ? config.deactivationText : "Unknown"} diff --git a/packages/studio-base/src/panels/ToggleSrvButton/index.stories.tsx b/packages/studio-base/src/panels/ToggleSrvButton/index.stories.tsx index d393990947..d7c34ee6fb 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/index.stories.tsx +++ b/packages/studio-base/src/panels/ToggleSrvButton/index.stories.tsx @@ -16,7 +16,6 @@ import { Config } from "./types"; const successResponseJson = JSON.stringify({ success: true }, undefined, 2); const baseConfig: Config = { serviceName: "/set_bool", - requestPayload: `{\n "data": true\n}`, }; const getFixture = ({ allowToggleSrvButton }: { allowToggleSrvButton: boolean }): Fixture => { @@ -104,9 +103,8 @@ export const ToggleSrvButtonEnabledWithCustomButtonSettings: StoryObj = { overrideConfig={{ ...baseConfig, buttonColor: "#ffbf49", - buttonTooltip: "I am a button tooltip", - buttonActive: "Activate that funky service", - buttonDisable: "Disable that funky service", + activationText: "Activate that funky service", + deactivationText: "Disable that funky service", }} /> ); @@ -122,19 +120,10 @@ export const ToggleSrvButtonEnabledWithCustomButtonSettings: StoryObj = { parameters: { panelSetup: { fixture: getFixture({ allowToggleSrvButton: true }) } }, }; -const validJSON = `{\n "a": 1,\n "b": 2,\n "c": 3\n}`; export const WithValidJSON: StoryObj = { render: () => { - return ; - }, -}; - -const invalidJSON = `{\n "a": 1,\n 'b: 2,\n "c": 3\n}`; - -export const WithInvalidJSON: StoryObj = { - render: () => { - return ; + return ; }, }; diff --git a/packages/studio-base/src/panels/ToggleSrvButton/settings.ts b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts index f2715b1525..f8a56c89f2 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/settings.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts @@ -13,9 +13,11 @@ import { Config } from "./types"; export const defaultConfig: Config = { serviceName: "", - stateFieldName: "", - buttonActive: "Activate", - buttonDisable: "Deactivate", + statusTopicName: "", + activationText: "Activate", + activationColor: "#090", + deactivationText: "Deactivate", + deactivationColor: "#900", }; function serviceError(serviceName?: string) { @@ -34,7 +36,12 @@ export function settingsActionReducer(prevConfig: Config, action: SettingsTreeAc }); } -export function useSettingsTree(config: Config): SettingsTreeNodes { +const supportedDataTypes = ["bool"]; + +export function useSettingsTree( + config: Config, + pathParseError: string | undefined, +): SettingsTreeNodes { const settings = useMemo( (): SettingsTreeNodes => ({ general: { @@ -43,31 +50,42 @@ export function useSettingsTree(config: Config): SettingsTreeNodes { label: "Service name", input: "string", error: serviceError(config.serviceName), - value: config.serviceName ?? "", + value: config.serviceName, }, - stateFieldName: { - label: "Topic State Field Name", - input: "string", - value: config.stateFieldName, + statusTopicName: { + label: "Current State Data", + input: "messagepath", + value: config.statusTopicName, + error: pathParseError, + validTypes: supportedDataTypes, }, }, }, button: { label: "Button", fields: { - buttonActive: { + activationText: { label: "Activation Message", input: "string", - value: config.buttonActive, + value: config.activationText, placeholder: "Activate", }, - buttonDisable: { + activationColor: { + label: "Activation Color", + input: "rgb", + value: config.activationColor, + }, + deactivationText: { label: "Deactivation Message", input: "string", - value: config.buttonDisable, + value: config.deactivationText, placeholder: "Deactivate", }, - buttonTooltip: { label: "Tooltip", input: "string", value: config.buttonTooltip }, + deactivationColor: { + label: "Deactivation Color", + input: "rgb", + value: config.deactivationColor, + }, }, }, }), diff --git a/packages/studio-base/src/panels/ToggleSrvButton/types.ts b/packages/studio-base/src/panels/ToggleSrvButton/types.ts index 37d3b7024c..6a661e9b42 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/types.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/types.ts @@ -4,8 +4,9 @@ export type Config = { serviceName: string; - stateFieldName: string; - buttonActive: string; - buttonDisable: string; - buttonTooltip?: string; + statusTopicName: string; + activationText: string; + activationColor: string; + deactivationText: string; + deactivationColor: string; }; From 1ab133cf8d091dcffba333e620a637725c5216ee Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Fri, 27 Dec 2024 16:41:01 +0100 Subject: [PATCH 11/18] Circle E-stop button --- .vscode/extensions.json | 1 - demo/panther-layout.json | 55 ++++++++++++++----- .../studio-base/src/panels/EStop/EStop.tsx | 8 +-- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 933d0069f7..cd3622fa75 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,7 +7,6 @@ "recommendations": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", - "orta.vscode-jest", "bierner.comment-tagged-templates" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. diff --git a/demo/panther-layout.json b/demo/panther-layout.json index a5285df87d..fd41eead0b 100644 --- a/demo/panther-layout.json +++ b/demo/panther-layout.json @@ -4,27 +4,27 @@ "minLevel": 0, "pinnedIds": [], "hardwareIdFilter": "", - "topicToRender": "{{env "ROBOT_NAMESPACE"}}/diagnostics", + "topicToRender": "/panther/diagnostics", "sortByLevel": true }, "Plot!dg5ynj": { "paths": [ { "timestampMethod": "receiveTime", - "value": "{{env "ROBOT_NAMESPACE"}}/imu/data.linear_acceleration.x", + "value": "/panther/imu/data.linear_acceleration.x", "enabled": true, "label": "x", "showLine": true }, { "timestampMethod": "receiveTime", - "value": "{{env "ROBOT_NAMESPACE"}}/imu/data.linear_acceleration.y", + "value": "/panther/imu/data.linear_acceleration.y", "enabled": true, "label": "y" }, { "timestampMethod": "receiveTime", - "value": "{{env "ROBOT_NAMESPACE"}}/imu/data.linear_acceleration.z", + "value": "/panther/imu/data.linear_acceleration.z", "enabled": true, "label": "z" } @@ -41,7 +41,7 @@ "followingViewWidth": 60 }, "Bar!3t52ye7": { - "path": "{{env "ROBOT_NAMESPACE"}}/joint_states.effort[0]", + "path": "/panther/joint_states.effort[0]", "maxValue": 34.52, "colorMode": "colormap", "gradient": ["#0000ff", "#ff00ff"], @@ -49,7 +49,7 @@ "foxglovePanelTitle": "FL" }, "Bar!461hl59": { - "path": "{{env "ROBOT_NAMESPACE"}}/joint_states.effort[1]", + "path": "/panther/joint_states.effort[1]", "maxValue": 34.52, "colorMode": "colormap", "gradient": ["#0000ff", "#ff00ff"], @@ -57,7 +57,7 @@ "foxglovePanelTitle": "FR" }, "Bar!1fzrnqw": { - "path": "{{env "ROBOT_NAMESPACE"}}/joint_states.effort[2]", + "path": "/panther/joint_states.effort[2]", "maxValue": 34.52, "colorMode": "colormap", "gradient": ["#0000ff", "#ff00ff"], @@ -65,15 +65,33 @@ "foxglovePanelTitle": "RL" }, "Bar!1q5qffy": { - "path": "{{env "ROBOT_NAMESPACE"}}/joint_states.effort[3]", + "path": "/panther/joint_states.effort[3]", "maxValue": 34.52, "colorMode": "colormap", "gradient": ["#0000ff", "#ff00ff"], "reverse": true, "foxglovePanelTitle": "RR" }, + "ToggleSrvButton!2dzr02u": { + "serviceName": "/panther/hardware/motor_power_enable", + "statusTopicName": "/panther/hardware/io_state.motor_on", + "activationText": "Activate Motors", + "activationColor": "#ae5312", + "deactivationText": "Deactivate Motors ", + "deactivationColor": "#826b0e", + "foxglovePanelTitle": "Motor Enable" + }, + "ToggleSrvButton!449y0td": { + "serviceName": "/panther/hardware/aux_power_enable", + "statusTopicName": "/panther/hardware/io_state.aux_power", + "activationText": "⚡Enable AUX⚡", + "activationColor": "#0069a6", + "deactivationText": "Disable AUX", + "deactivationColor": "#429900", + "foxglovePanelTitle": "AUX power" + }, "Battery!wppv5y": { - "path": "{{env "ROBOT_NAMESPACE"}}/battery/battery_status.percentage", + "path": "/panther/battery/battery_status.percentage", "minValue": 0, "maxValue": 1, "colorMap": "red-yellow-green", @@ -87,13 +105,13 @@ "layout": "vertical", "advancedView": false, "serviceName": "", - "goServiceName": "{{env "ROBOT_NAMESPACE"}}/hardware/e_stop_reset", - "stopServiceName": "{{env "ROBOT_NAMESPACE"}}/hardware/e_stop_trigger", - "statusTopicName": "{{env "ROBOT_NAMESPACE"}}/hardware/e_stop.data", + "goServiceName": "/panther/hardware/e_stop_reset", + "stopServiceName": "/panther/hardware/e_stop_trigger", + "statusTopicName": "/panther/hardware/e_stop.data", "foxglovePanelTitle": "E-stop" }, "Joy!3fmstz6": { - "topic": "{{env "ROBOT_NAMESPACE"}}/cmd_vel", + "topic": "/panther/cmd_vel", "publishRate": 5, "upButton": { "field": "linear-x", @@ -127,7 +145,8 @@ "field": "angular-z", "limit": 3 }, - "stamped": false + "stamped": false, + "advanced": false }, "Tab!1plmth0": { "activeTabIdx": 2, @@ -155,6 +174,14 @@ }, "direction": "column" } + }, + { + "title": "Services", + "layout": { + "first": "ToggleSrvButton!2dzr02u", + "second": "ToggleSrvButton!449y0td", + "direction": "row" + } } ] } diff --git a/packages/studio-base/src/panels/EStop/EStop.tsx b/packages/studio-base/src/panels/EStop/EStop.tsx index 82e08cb24b..341326588e 100644 --- a/packages/studio-base/src/panels/EStop/EStop.tsx +++ b/packages/studio-base/src/panels/EStop/EStop.tsx @@ -331,10 +331,10 @@ function EStopContent( onClick={eStopClicked} data-testid="call-service-button" style={{ - minWidth: "150px", - minHeight: "70px", - fontSize: "1.7rem", - borderRadius: "0.3rem", + minWidth: "100px", + minHeight: "100px", + fontSize: "2.2rem", + borderRadius: "50%", }} > {eStopAction?.toUpperCase() ?? "Wait for feedback"} From 768863b5eae567b2477a2e21b88b2e144fb142e1 Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Mon, 30 Dec 2024 14:02:15 +0100 Subject: [PATCH 12/18] Bigger e-stop --- packages/studio-base/src/panels/EStop/EStop.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/studio-base/src/panels/EStop/EStop.tsx b/packages/studio-base/src/panels/EStop/EStop.tsx index 341326588e..88e8a11d48 100644 --- a/packages/studio-base/src/panels/EStop/EStop.tsx +++ b/packages/studio-base/src/panels/EStop/EStop.tsx @@ -47,8 +47,8 @@ type Action = | { type: "path"; path: string } | { type: "seek" }; -const useStyles = makeStyles<{ state?: string }>()((theme, { state }) => { - const buttonColor = state === "go" ? "#090" : "#900"; +const useStyles = makeStyles<{ state: EStopState }>()((theme, { state }) => { + const buttonColor = state === "go" ? "#090" : state === "stop" ? "#900" : "#666"; const augmentedButtonColor = theme.palette.augmentColor({ color: { main: buttonColor }, }); @@ -331,8 +331,8 @@ function EStopContent( onClick={eStopClicked} data-testid="call-service-button" style={{ - minWidth: "100px", - minHeight: "100px", + minWidth: "150px", + minHeight: "150px", fontSize: "2.2rem", borderRadius: "50%", }} From 3c67363e1eab8e61793cfadababf78ba84d13639 Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Tue, 7 Jan 2025 17:02:52 +0100 Subject: [PATCH 13/18] Add reverse logic option --- .../ToggleSrvButton/ToggleSrvButton.tsx | 20 ++++++++++--------- .../src/panels/ToggleSrvButton/settings.ts | 5 +++++ .../src/panels/ToggleSrvButton/types.ts | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx index e24ce904cf..b901561292 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx +++ b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx @@ -265,19 +265,21 @@ function ToggleSrvButtonContent( } try { - setSrvState({ status: "requesting", value: `Calling ${config.serviceName}...` }); - const requestPayload = { data: buttonAction ?? true }; - setButtonAction(undefined); - const response = await context.callService(config.serviceName, requestPayload) as { success?: boolean }; - setSrvState({ - status: "success", - value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", - }); + if (buttonAction != undefined) { + setSrvState({ status: "requesting", value: `Calling ${config.serviceName}...` }); + const requestPayload = { data: config.reverseLogic ? !buttonAction : buttonAction }; + setButtonAction(undefined); + const response = await context.callService(config.serviceName, requestPayload) as { success?: boolean }; + setSrvState({ + status: "success", + value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", + }); + } } catch (err) { setSrvState({ status: "error", value: (err as Error).message }); log.error(err); } - }, [context, buttonAction, config.serviceName]); + }, [context, buttonAction, config]); // Setting buttonAction based on state.latestMatchingQueriedData useEffect(() => { diff --git a/packages/studio-base/src/panels/ToggleSrvButton/settings.ts b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts index f8a56c89f2..a7ecfc88ab 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/settings.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts @@ -59,6 +59,11 @@ export function useSettingsTree( error: pathParseError, validTypes: supportedDataTypes, }, + reverseLogic: { + label: "Reverse state logic", + input: "boolean", + value: config.reverseLogic, + }, }, }, button: { diff --git a/packages/studio-base/src/panels/ToggleSrvButton/types.ts b/packages/studio-base/src/panels/ToggleSrvButton/types.ts index 6a661e9b42..4b910c4b32 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/types.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/types.ts @@ -5,6 +5,7 @@ export type Config = { serviceName: string; statusTopicName: string; + reverseLogic: boolean; activationText: string; activationColor: string; deactivationText: string; From bfc2de70c2584c1dc910fb93b93add49a3d5c892 Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Tue, 7 Jan 2025 21:57:37 +0100 Subject: [PATCH 14/18] Fix --- packages/studio-base/src/panels/ToggleSrvButton/settings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/studio-base/src/panels/ToggleSrvButton/settings.ts b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts index a7ecfc88ab..4e6510c643 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/settings.ts +++ b/packages/studio-base/src/panels/ToggleSrvButton/settings.ts @@ -14,6 +14,7 @@ import { Config } from "./types"; export const defaultConfig: Config = { serviceName: "", statusTopicName: "", + reverseLogic: false, activationText: "Activate", activationColor: "#090", deactivationText: "Deactivate", From 17e3f47d490234f126aa7a2fdb29c4e1239fdb6e Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Tue, 7 Jan 2025 22:54:35 +0100 Subject: [PATCH 15/18] Add button state --- .../panels/ToggleSrvButton/ToggleSrvButton.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx index b901561292..ab5ed696e4 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx +++ b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx @@ -26,6 +26,8 @@ type Props = { context: PanelExtensionContext; }; +type ButtonState = "activated" | "deactivated" | undefined; + type SrvState = { status: "requesting" | "error" | "success"; value: string; @@ -45,8 +47,8 @@ type Action = | { type: "path"; path: string } | { type: "seek" }; -const useStyles = makeStyles<{ action?: boolean; config: Config }>()((theme, { action, config }) => { - const buttonColor = action === true ? config.activationColor : config.deactivationColor; +const useStyles = makeStyles<{ action?: ButtonState; config: Config }>()((theme, { action, config }) => { + const buttonColor = action === "activated" ? config.activationColor : config.deactivationColor; const augmentedButtonColor = theme.palette.augmentColor({ color: { main: buttonColor }, }); @@ -160,7 +162,7 @@ function ToggleSrvButtonContent( ...defaultConfig, ...(context.initialState as Partial), })); - const [buttonAction, setButtonAction] = useState(undefined); + const [buttonAction, setButtonAction] = useState(undefined); const { classes } = useStyles({ action: buttonAction, config }); const [state, dispatch] = useReducer( @@ -267,7 +269,7 @@ function ToggleSrvButtonContent( try { if (buttonAction != undefined) { setSrvState({ status: "requesting", value: `Calling ${config.serviceName}...` }); - const requestPayload = { data: config.reverseLogic ? !buttonAction : buttonAction }; + const requestPayload = { data: buttonAction === "activated" ? false : true }; setButtonAction(undefined); const response = await context.callService(config.serviceName, requestPayload) as { success?: boolean }; setSrvState({ @@ -285,9 +287,10 @@ function ToggleSrvButtonContent( useEffect(() => { const data = state.latestMatchingQueriedData; if (typeof data === "boolean") { - setButtonAction(!data); + const isDeactivated = data === config.reverseLogic; + setButtonAction(isDeactivated ? "deactivated" : "activated"); } - }, [state.latestMatchingQueriedData]); + }, [state.latestMatchingQueriedData, config.reverseLogic]); // Indicate render is complete - the effect runs after the dom is updated useEffect(() => { @@ -325,7 +328,7 @@ function ToggleSrvButtonContent( borderRadius: "0.3rem", }} > - {buttonAction === true ? config.activationText : buttonAction === false ? config.deactivationText : "Unknown"} + {buttonAction === "activated" ? config.deactivationText : buttonAction === "deactivated" ? config.activationText : "Unknown"} From e98e5f3601a0c98f69c18e75830eb525bb7a7259 Mon Sep 17 00:00:00 2001 From: Rafal Gorecki <126687345+rafal-gorecki@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:46:51 +0100 Subject: [PATCH 16/18] Add services notification and unify colors (#4) --- .../studio-base/src/panels/Battery/styles.css | 3 +- .../studio-base/src/panels/EStop/EStop.tsx | 144 +++++++++--------- .../studio-base/src/panels/Joy/JoyVisual.tsx | 16 +- .../studio-base/src/panels/Joy/styles.css | 44 ++---- .../ToggleSrvButton/ToggleSrvButton.tsx | 128 ++++++++-------- 5 files changed, 164 insertions(+), 171 deletions(-) diff --git a/packages/studio-base/src/panels/Battery/styles.css b/packages/studio-base/src/panels/Battery/styles.css index 6800c9d814..69e68117a6 100644 --- a/packages/studio-base/src/panels/Battery/styles.css +++ b/packages/studio-base/src/panels/Battery/styles.css @@ -74,6 +74,7 @@ body { .battery__text { margin-bottom: 0.3rem; + font-size: var(--normal-font-size); } .battery__percentage { @@ -91,7 +92,7 @@ body { } .battery__status i { - font-size: 1.25rem; + font-size: var(--normal-font-size); } .battery__pill { diff --git a/packages/studio-base/src/panels/EStop/EStop.tsx b/packages/studio-base/src/panels/EStop/EStop.tsx index 88e8a11d48..37bc0e16d2 100644 --- a/packages/studio-base/src/panels/EStop/EStop.tsx +++ b/packages/studio-base/src/panels/EStop/EStop.tsx @@ -7,10 +7,10 @@ import * as _ from "lodash-es"; import { Dispatch, SetStateAction, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useState } from "react"; import { makeStyles } from "tss-react/mui"; -import Log from "@foxglove/log"; import { parseMessagePath, MessagePath } from "@foxglove/message-path"; import { MessageEvent, PanelExtensionContext, SettingsTreeAction } from "@foxglove/studio"; import { simpleGetMessagePathDataItems } from "@foxglove/studio-base/components/MessagePathSyntax/simpleGetMessagePathDataItems"; +import NotificationModal from "@foxglove/studio-base/components/NotificationModal"; import Stack from "@foxglove/studio-base/components/Stack"; import { Config } from "@foxglove/studio-base/panels/EStop/types"; import ThemeProvider from "@foxglove/studio-base/theme/ThemeProvider"; @@ -19,18 +19,16 @@ import { defaultConfig, settingsActionReducer, useSettingsTree } from "./setting import "./styles.css"; - -const log = Log.getLogger(__dirname); - type Props = { context: PanelExtensionContext; }; type EStopState = "go" | "stop" | undefined; +type SrvResponse = { success: boolean; message: string }; -type ReqState = { +type SrvState = { status: "requesting" | "error" | "success"; - value: string; + response: SrvResponse | undefined; }; type State = { @@ -161,7 +159,7 @@ function EStopContent( // panel extensions must notify when they've completed rendering // onRender will setRenderDone to a done callback which we can invoke after we've rendered const [renderDone, setRenderDone] = useState<() => void>(() => () => { }); - const [reqState, setReqState] = useState(); + const [srvState, setSrvState] = useState(); const [eStopAction, setEStopAction] = useState(); const [config, setConfig] = useState(() => ({ ...defaultConfig, @@ -186,33 +184,28 @@ function EStopContent( dispatch({ type: "path", path: config.statusTopicName }); }, [config.statusTopicName]); - useEffect(() => { - context.saveState(config); - context.setDefaultPanelTitle( - config.goServiceName ? `Unspecified` : undefined, - ); - }, [config, context]); + const handleRequestCloseNotification = () => { + setSrvState(undefined); + }; useEffect(() => { context.saveState(config); - context.setDefaultPanelTitle( - config.stopServiceName ? `Unspecified` : undefined, - ); + context.setDefaultPanelTitle(`E-Stop`); }, [config, context]); useEffect(() => { context.watch("colorScheme"); - context.onRender = (renderReqState, done) => { + context.onRender = (renderSrvState, done) => { setRenderDone(() => done); - setColorScheme(renderReqState.colorScheme ?? "light"); + setColorScheme(renderSrvState.colorScheme ?? "light"); - if (renderReqState.didSeek === true) { + if (renderSrvState.didSeek === true) { dispatch({ type: "seek" }); } - if (renderReqState.currentFrame) { - dispatch({ type: "frame", messages: renderReqState.currentFrame }); + if (renderSrvState.currentFrame) { + dispatch({ type: "frame", messages: renderSrvState.currentFrame }); } }; @@ -263,37 +256,29 @@ function EStopContent( config.goServiceName && config.stopServiceName && eStopAction != undefined && - reqState?.status !== "requesting", + srvState?.status !== "requesting", ); const eStopClicked = useCallback(async () => { if (!context.callService) { - setReqState({ status: "error", value: "The data source does not allow calling services" }); + setSrvState({ status: "error", response: undefined }); return; } const serviceName = eStopAction === "go" ? config.goServiceName : config.stopServiceName; if (!serviceName) { - setReqState({ status: "error", value: "Service name is not configured" }); + setSrvState({ status: "error", response: undefined }); return; } - try { - setReqState({ status: "requesting", value: `Calling ${serviceName}...` }); - setEStopAction(undefined); - const response = await context.callService(serviceName, {}); - setReqState({ - status: "success", - value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", - }); - } catch (err) { - setReqState({ status: "error", value: (err as Error).message }); - log.error(err); - } + setSrvState({ status: "requesting", response: undefined }); + const response = await context.callService(serviceName, {}) as SrvResponse; + setSrvState({ status: "success", response }); + }, [context, eStopAction, config.goServiceName, config.stopServiceName]); - // Setting eStopAction based on state.latestMatchingQueriedData + // Setting eStopAction based on received state useEffect(() => { if (state.latestMatchingQueriedData != undefined) { const data = state.latestMatchingQueriedData as boolean; @@ -307,42 +292,55 @@ function EStopContent( }, [renderDone]); return ( - - -
- - {statusMessage && ( - - {statusMessage} - - )} - - - - -
+ <> + + +
+ + {statusMessage && ( + + {statusMessage} + + )} + + + + +
+
-
+ {srvState?.response?.success === false && ( + + )} + ); } diff --git a/packages/studio-base/src/panels/Joy/JoyVisual.tsx b/packages/studio-base/src/panels/Joy/JoyVisual.tsx index 3f9af93e75..4d34d7862e 100644 --- a/packages/studio-base/src/panels/Joy/JoyVisual.tsx +++ b/packages/studio-base/src/panels/Joy/JoyVisual.tsx @@ -145,17 +145,23 @@ function JoyVisual(props: JoyVisualProps): JSX.Element { return (
- + - + + + - {advanced && (
-
({speed?.x.toFixed(2) ?? "0.00"}, {speed?.y.toFixed(2) ?? "0.00"})
-
)} + {advanced && ( +
+
+ ({speed?.x.toFixed(2) ?? "0.00"}, {speed?.y.toFixed(2) ?? "0.00"}) +
+
+ )}
{advanced && (
diff --git a/packages/studio-base/src/panels/Joy/styles.css b/packages/studio-base/src/panels/Joy/styles.css index 14a55582d9..ef34b6009a 100644 --- a/packages/studio-base/src/panels/Joy/styles.css +++ b/packages/studio-base/src/panels/Joy/styles.css @@ -5,8 +5,10 @@ } body { - --joystick-color: #888; - --joystick-head-color: #f64; + --joystick-color: #777; + --joystick-color-augmented: #444; + --joystick-head-color: #f52; + --joystick-head-color-augmented: #f63; } #container { @@ -19,16 +21,6 @@ body { padding: 2em; } -#toggle-editing { - position: absolute; - top: 1em; - left: 1em; - font-size: 1.5em; - border-radius: 1em; - cursor: pointer; - transition: #3498db 0.3s ease; -} - #joystick-container { display: flex; flex-direction: column; @@ -45,28 +37,24 @@ body { .joystick-background { fill: var(--joystick-color); - animation: animateCircle 2s infinite; stroke-width: 2; + stroke: green; +} + +.joystick-handle-group { + transform-origin: 50% 50%; + animation: scalingAnimation 2s infinite ease-in-out; } -/* App.css */ -@keyframes animateCircle { +@keyframes scalingAnimation { 0% { - stroke: #26b355; - stroke-width: 2; - } - 25% { - stroke: #2dd565; + transform: scale(1); } 50% { - stroke: #2fde69; - stroke-width: 3; - } - 75% { - stroke: #2dd565; + transform: scale(1.1); } 100% { - stroke: #26b355; + transform: scale(1); } } @@ -76,11 +64,11 @@ body { } .joystick-handle:hover { - fill: #ff6d4d; + fill: var(--joystick-head-color-augmented); } .joystick-triangle { - fill: var(--joystick-color); + fill: var(--joystick-color-augmented); } #joystick-position { diff --git a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx index ab5ed696e4..419962d7ee 100644 --- a/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx +++ b/packages/studio-base/src/panels/ToggleSrvButton/ToggleSrvButton.tsx @@ -7,10 +7,10 @@ import * as _ from "lodash-es"; import { Dispatch, SetStateAction, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useState } from "react"; import { makeStyles } from "tss-react/mui"; -import Log from "@foxglove/log"; import { parseMessagePath, MessagePath } from "@foxglove/message-path"; import { MessageEvent, PanelExtensionContext, SettingsTreeAction } from "@foxglove/studio"; import { simpleGetMessagePathDataItems } from "@foxglove/studio-base/components/MessagePathSyntax/simpleGetMessagePathDataItems"; +import NotificationModal from "@foxglove/studio-base/components/NotificationModal"; import Stack from "@foxglove/studio-base/components/Stack"; import { Config } from "@foxglove/studio-base/panels/ToggleSrvButton/types"; import ThemeProvider from "@foxglove/studio-base/theme/ThemeProvider"; @@ -19,18 +19,16 @@ import { defaultConfig, settingsActionReducer, useSettingsTree } from "./setting import "./styles.css"; - -const log = Log.getLogger(__dirname); - type Props = { context: PanelExtensionContext; }; type ButtonState = "activated" | "deactivated" | undefined; +type SrvResponse = { success: boolean; message: string }; type SrvState = { status: "requesting" | "error" | "success"; - value: string; + response: SrvResponse | undefined; }; type State = { @@ -48,7 +46,7 @@ type Action = | { type: "seek" }; const useStyles = makeStyles<{ action?: ButtonState; config: Config }>()((theme, { action, config }) => { - const buttonColor = action === "activated" ? config.activationColor : config.deactivationColor; + const buttonColor = action === "activated" ? config.deactivationColor : config.activationColor; const augmentedButtonColor = theme.palette.augmentColor({ color: { main: buttonColor }, }); @@ -178,16 +176,14 @@ function ToggleSrvButtonContent( }), ); + const handleRequestCloseNotification = () => { + setSrvState(undefined); + }; + useLayoutEffect(() => { dispatch({ type: "path", path: config.statusTopicName }); }, [config.statusTopicName]); - useEffect(() => { - context.saveState(config); - context.setDefaultPanelTitle( - config.statusTopicName === "" ? undefined : config.statusTopicName); - }, [config, context]); - useEffect(() => { context.saveState(config); context.setDefaultPanelTitle( @@ -262,28 +258,19 @@ function ToggleSrvButtonContent( const toggleSrvButtonClicked = useCallback(async () => { if (!context.callService) { - setSrvState({ status: "error", value: "The data source does not allow calling services" }); + setSrvState({ status: "error", response: undefined }); return; } - try { - if (buttonAction != undefined) { - setSrvState({ status: "requesting", value: `Calling ${config.serviceName}...` }); - const requestPayload = { data: buttonAction === "activated" ? false : true }; - setButtonAction(undefined); - const response = await context.callService(config.serviceName, requestPayload) as { success?: boolean }; - setSrvState({ - status: "success", - value: JSON.stringify(response, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2) ?? "", - }); - } - } catch (err) { - setSrvState({ status: "error", value: (err as Error).message }); - log.error(err); + if (buttonAction != undefined) { + setSrvState({ status: "requesting", response: undefined }); + const requestPayload = { data: buttonAction === "activated" ? false : true }; + const response = await context.callService(config.serviceName, requestPayload) as SrvResponse; + setSrvState({ status: "success", response }); } }, [context, buttonAction, config]); - // Setting buttonAction based on state.latestMatchingQueriedData + // Setting buttonAction based on received state useEffect(() => { const data = state.latestMatchingQueriedData; if (typeof data === "boolean") { @@ -298,42 +285,55 @@ function ToggleSrvButtonContent( }, [renderDone]); return ( - - -
- - {statusMessage && ( - - {statusMessage} - - )} - - - - -
+ <> + + +
+ + {statusMessage && ( + + {statusMessage} + + )} + + + + +
+
-
+ {srvState?.response?.success === false && ( + + )} + ); } From 93b68a36b8e41317c7e37723a73bff826d4f731d Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Wed, 8 Jan 2025 17:17:50 +0100 Subject: [PATCH 17/18] Remove unnecessary --- demo/compose.yaml | 14 ------ demo/panther-layout.json | 55 ++++++---------------- packages/studio-base/src/i18n/en/panels.ts | 2 +- 3 files changed, 15 insertions(+), 56 deletions(-) diff --git a/demo/compose.yaml b/demo/compose.yaml index 457c74825e..99143460a5 100644 --- a/demo/compose.yaml +++ b/demo/compose.yaml @@ -28,17 +28,3 @@ services: ports: - 8765:8765 command: ros2 launch foxglove_bridge foxglove_bridge_launch.xml port:=8765 capabilities:=[clientPublish,parameters,parametersSubscribe,services,connectionGraph,assets] - - ros: - build: - context: . - dockerfile: Dockerfile - container_name: ros-test - <<: *common-config - command: tail -f /dev/null - - # docker exec -it ros-test bash - # source /opt/ros/humble/setup.bash - # ros2 topic echo /cmd_vel geometry_msgs/msg/TwistStamped - - # docker compose up --build --force-recreate -d diff --git a/demo/panther-layout.json b/demo/panther-layout.json index fd41eead0b..a5285df87d 100644 --- a/demo/panther-layout.json +++ b/demo/panther-layout.json @@ -4,27 +4,27 @@ "minLevel": 0, "pinnedIds": [], "hardwareIdFilter": "", - "topicToRender": "/panther/diagnostics", + "topicToRender": "{{env "ROBOT_NAMESPACE"}}/diagnostics", "sortByLevel": true }, "Plot!dg5ynj": { "paths": [ { "timestampMethod": "receiveTime", - "value": "/panther/imu/data.linear_acceleration.x", + "value": "{{env "ROBOT_NAMESPACE"}}/imu/data.linear_acceleration.x", "enabled": true, "label": "x", "showLine": true }, { "timestampMethod": "receiveTime", - "value": "/panther/imu/data.linear_acceleration.y", + "value": "{{env "ROBOT_NAMESPACE"}}/imu/data.linear_acceleration.y", "enabled": true, "label": "y" }, { "timestampMethod": "receiveTime", - "value": "/panther/imu/data.linear_acceleration.z", + "value": "{{env "ROBOT_NAMESPACE"}}/imu/data.linear_acceleration.z", "enabled": true, "label": "z" } @@ -41,7 +41,7 @@ "followingViewWidth": 60 }, "Bar!3t52ye7": { - "path": "/panther/joint_states.effort[0]", + "path": "{{env "ROBOT_NAMESPACE"}}/joint_states.effort[0]", "maxValue": 34.52, "colorMode": "colormap", "gradient": ["#0000ff", "#ff00ff"], @@ -49,7 +49,7 @@ "foxglovePanelTitle": "FL" }, "Bar!461hl59": { - "path": "/panther/joint_states.effort[1]", + "path": "{{env "ROBOT_NAMESPACE"}}/joint_states.effort[1]", "maxValue": 34.52, "colorMode": "colormap", "gradient": ["#0000ff", "#ff00ff"], @@ -57,7 +57,7 @@ "foxglovePanelTitle": "FR" }, "Bar!1fzrnqw": { - "path": "/panther/joint_states.effort[2]", + "path": "{{env "ROBOT_NAMESPACE"}}/joint_states.effort[2]", "maxValue": 34.52, "colorMode": "colormap", "gradient": ["#0000ff", "#ff00ff"], @@ -65,33 +65,15 @@ "foxglovePanelTitle": "RL" }, "Bar!1q5qffy": { - "path": "/panther/joint_states.effort[3]", + "path": "{{env "ROBOT_NAMESPACE"}}/joint_states.effort[3]", "maxValue": 34.52, "colorMode": "colormap", "gradient": ["#0000ff", "#ff00ff"], "reverse": true, "foxglovePanelTitle": "RR" }, - "ToggleSrvButton!2dzr02u": { - "serviceName": "/panther/hardware/motor_power_enable", - "statusTopicName": "/panther/hardware/io_state.motor_on", - "activationText": "Activate Motors", - "activationColor": "#ae5312", - "deactivationText": "Deactivate Motors ", - "deactivationColor": "#826b0e", - "foxglovePanelTitle": "Motor Enable" - }, - "ToggleSrvButton!449y0td": { - "serviceName": "/panther/hardware/aux_power_enable", - "statusTopicName": "/panther/hardware/io_state.aux_power", - "activationText": "⚡Enable AUX⚡", - "activationColor": "#0069a6", - "deactivationText": "Disable AUX", - "deactivationColor": "#429900", - "foxglovePanelTitle": "AUX power" - }, "Battery!wppv5y": { - "path": "/panther/battery/battery_status.percentage", + "path": "{{env "ROBOT_NAMESPACE"}}/battery/battery_status.percentage", "minValue": 0, "maxValue": 1, "colorMap": "red-yellow-green", @@ -105,13 +87,13 @@ "layout": "vertical", "advancedView": false, "serviceName": "", - "goServiceName": "/panther/hardware/e_stop_reset", - "stopServiceName": "/panther/hardware/e_stop_trigger", - "statusTopicName": "/panther/hardware/e_stop.data", + "goServiceName": "{{env "ROBOT_NAMESPACE"}}/hardware/e_stop_reset", + "stopServiceName": "{{env "ROBOT_NAMESPACE"}}/hardware/e_stop_trigger", + "statusTopicName": "{{env "ROBOT_NAMESPACE"}}/hardware/e_stop.data", "foxglovePanelTitle": "E-stop" }, "Joy!3fmstz6": { - "topic": "/panther/cmd_vel", + "topic": "{{env "ROBOT_NAMESPACE"}}/cmd_vel", "publishRate": 5, "upButton": { "field": "linear-x", @@ -145,8 +127,7 @@ "field": "angular-z", "limit": 3 }, - "stamped": false, - "advanced": false + "stamped": false }, "Tab!1plmth0": { "activeTabIdx": 2, @@ -174,14 +155,6 @@ }, "direction": "column" } - }, - { - "title": "Services", - "layout": { - "first": "ToggleSrvButton!2dzr02u", - "second": "ToggleSrvButton!449y0td", - "direction": "row" - } } ] } diff --git a/packages/studio-base/src/i18n/en/panels.ts b/packages/studio-base/src/i18n/en/panels.ts index 8ecc07d1c1..21fb7d1323 100644 --- a/packages/studio-base/src/i18n/en/panels.ts +++ b/packages/studio-base/src/i18n/en/panels.ts @@ -50,7 +50,7 @@ export const panels = { teleopDescription: "Teleoperate a robot over a live connection.", topicGraph: "Topic Graph", topicGraphDescription: "Display a graph of active nodes, topics, and services.", - toggleSrvButton: "Custom: Toggle Srv Button", + toggleSrvButton: "Custom: Toggle Service Button", toggleSrvButtonDescription: "Button to call services.", userScripts: "User Scripts", userScriptsDescription: From f240de963da0b96e65458f65fc346aa8025f0b74 Mon Sep 17 00:00:00 2001 From: rafal-gorecki Date: Fri, 10 Jan 2025 16:33:53 +0100 Subject: [PATCH 18/18] adjust color --- packages/studio-base/src/panels/Joy/styles.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/studio-base/src/panels/Joy/styles.css b/packages/studio-base/src/panels/Joy/styles.css index ef34b6009a..80497b5918 100644 --- a/packages/studio-base/src/panels/Joy/styles.css +++ b/packages/studio-base/src/panels/Joy/styles.css @@ -7,7 +7,7 @@ body { --joystick-color: #777; --joystick-color-augmented: #444; - --joystick-head-color: #f52; + --joystick-head-color: #e52; --joystick-head-color-augmented: #f63; } @@ -38,7 +38,7 @@ body { .joystick-background { fill: var(--joystick-color); stroke-width: 2; - stroke: green; + stroke: #1b1; } .joystick-handle-group {