Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# param_ext version history

## 0.0.03

- Fix issue with empty parameters

## 0.0.2

- Alphabetize nodes

## 0.0.1

- Alpha testing
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

## _A Foxglove Studio Extension_

The **ROS2 Parameters Extension** provides parameter functionality for all your ROS2 nodes including:
The **ROS2 Parameters Extension** provides parameter functionality for all your ROS2 nodes including:

- **View** a node's parameter names, types, and values in a table format
- **Set** new parameter values for all types of parameters on a node
- **Load** a previous configuration from a .yml file stored on your computer

Currently only works with a rosbridge connection
This plugin relies on the `/rosapi/nodes` service to be available on the robot in order to build a list of running nodes.
This can either be done by making sure [rosbridge_server](https://github.com/RobotWebTools/rosbridge_suite/blob/ros2/rosbridge_server/launch/rosbridge_websocket_launch.xml) is running:

```ros2 launch rosbridge_server rosbridge_websocket_launch.xml```

or solely the [rosapi node](https://github.com/RobotWebTools/rosbridge_suite/blob/ros2/rosapi/scripts/rosapi_node):

```ros2 run rosapi rosapi_node```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Interact with Parameters in ROS2",
"publisher": "Daniel Clapp",
"homepage": "https://github.com/danclapp4/ros2-parameter-extension",
"version": "0.0.1",
"version": "0.0.3",
"license": "MIT",
"main": "./dist/extension.js",
"keywords": [],
Expand Down
137 changes: 84 additions & 53 deletions src/ExamplePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem

useLayoutEffect( () => {

context.onRender = (renderState: RenderState, done) => {
context.onRender = (renderState: RenderState, done) => {

setRenderDone(() => done);
setRenderDone(() => done);
updateNodeList();

//Manage some styling for light and dark theme
Expand All @@ -53,7 +53,7 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
}, []);

// invoke the done callback once the render is complete
useEffect(() => {
useEffect(() => {
renderDone?.();
}, [renderDone]);

Expand All @@ -72,7 +72,7 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem

/**
* determines if a string[] contains exlusively booleans
* @param strArr string[] to check
* @param strArr string[] to check
* @returns true if strArr only contains booleans, false otherwise
*/
const isBooleanArr = (strArr: string[]) => {
Expand Down Expand Up @@ -114,13 +114,13 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
const updateNodeList = () => {
setStatus("retreiving nodes...")
context.callService?.("/rosapi/nodes", {})
.then((_values: unknown) =>{
setNodeList((_values as any).nodes as string[]);
setStatus("nodes retreived");
.then((_values: unknown) =>{
setNodeList(((_values as any).nodes as string[]).sort());
setStatus("nodes retreived");
})
.catch((_error: Error) => { setStatus(_error.toString()); });
}

/**
* Retrieves a list of all parameters for the current node and their values
*/
Expand All @@ -138,7 +138,7 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
for (let i = 0; i < paramNameList.length; i++) {
tempList.push({name: paramNameList[i]!, value: paramValList[i]!});
}
if(tempList.length > 0)
if(tempList.length > 0)
setParamList(tempList);

if(paramNameList !== undefined) {
Expand Down Expand Up @@ -193,7 +193,7 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
}

/**
* Update the list of Parameters with new values to be set
* Update the list of Parameters with new values to be set
* @param val The new value to be set
* @param name The name of the parameter that will be set to 'val'
*/
Expand All @@ -206,28 +206,51 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
const emptyP: SetSrvParam = {};
tempList[idx] = emptyP;
} else {
let ssp: SetSrvParam = {};
// Set default ssp to complete parameter structure (https://github.com/foxglove/ros-foxglove-bridge/issues/333)
let ssp: SetSrvParam = {
name: "",
value: {
type: 0,
bool_value: false,
integer_value: 0,
double_value: 0.0,
string_value: "",
byte_array_value: [],
bool_array_value: [],
integer_array_value: [],
double_array_value: [],
string_array_value: [],
},
};
let valStrArr: string[] = [];
switch (paramList![idx]?.value.type!) {
case 1:
ssp = { name: name, value: { type: 1, bool_value: stringToBoolean(val) }};
case 1:
ssp.name = name;
ssp.value!.type = 1;
ssp.value!.bool_value = stringToBoolean(val);
break;

case 2:
ssp = { name: name, value: { type: 2, integer_value: +val }};
case 2:
ssp.name = name;
ssp.value!.type = 2;
ssp.value!.integer_value = +val;
break;

case 3:
ssp = { name: name, value: { type: 3, double_value: +val }};
case 3:
ssp.name = name;
ssp.value!.type = 3;
ssp.value!.double_value = +val;
break;

case 4:
ssp = { name: name, value: { type: 4, string_value: val }};
case 4:
ssp.name = name;
ssp.value!.type = 4;
ssp.value!.string_value = val;
break;

// TODO: Implement format for byte arrays
case 5:
//ssp = { name: name, value: { type: 5, byte_array_value: val as unknown as number[] }};
case 5:
//ssp = { name: name, value: { type: 5, byte_array_value: val as unknown as number[] }};
break;

case 6:
Expand All @@ -238,26 +261,34 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
return true;
return false;
});
ssp = { name: name, value: { type: 6, bool_array_value: valBoolArr}};
ssp.name = name;
ssp.value!.type = 6;
ssp.value!.bool_array_value = valBoolArr;
}
break;

case 7:
valStrArr = val.replace(" ", "").replace("[", "").replace("]", "").split(",");
ssp = { name: name, value: { type: 7, integer_array_value: valStrArr.map(Number) }};
case 7:
valStrArr = val.replace(" ", "").replace("[", "").replace("]", "").split(",");
ssp.name = name;
ssp.value!.type = 7;
ssp.value!.integer_array_value = valStrArr.map(Number);
break;

case 8:
valStrArr = val.replace(" ", "").replace("[", "").replace("]", "").split(",");
ssp = { name: name, value: { type: 8, double_array_value: valStrArr.map(Number) }};
ssp.name = name;
ssp.value!.type = 8;
ssp.value!.double_array_value = valStrArr.map(Number);
break;

case 9:
val.replace(" ", "");
if(val.charAt(0) == '[' && val.charAt(val.length - 1) == ']')
if(val.charAt(0) == '[' && val.charAt(val.length - 1) == ']')
val = val.substring(1, val.length - 1);
valStrArr = val.split(",");
ssp = { name: name, value: { type: 9, string_array_value: valStrArr }};
ssp.name = name;
ssp.value!.type = 9;
ssp.value!.string_array_value = valStrArr;
break;

default: ssp = {}; break;
Expand All @@ -270,7 +301,7 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
tempValList.push(getParameterValue(element));
});
}

/**
* Creates a dropdown input box if param is a boolean, creates a text input box otherwise
* @param param The parameter that an input box is being created for
Expand All @@ -290,7 +321,7 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
);
}
return(
<input style={inputStyle} placeholder={getParameterValue(param.value)} onChange={(event) => { updateSrvParamList(param.name, event.target.value) }}/>
<input style={inputStyle} placeholder={getParameterValue(param.value)} onChange={(event) => { updateSrvParamList(param.name, event.target.value) }}/>
);
}

Expand All @@ -299,10 +330,10 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
* loads parameter values from a YAML file and sets all new values
* @param files the YAML file to be uploaded
*/
const loadFile = (files: FileList | null) => {
const loadFile = (files: FileList | null) => {
if(files !== null) {
files[0]?.text()
.then((value: string) => {
.then((value: string) => {
value = value.replaceAll(/[^\S\r\n]/gi, "");
value = value.replace(node + ":\n", "");
value = value.replace("ros__parameters:\n", "");
Expand Down Expand Up @@ -435,7 +466,7 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
borderRadius: "3px",

};

inputStyle = {

fontSize: "1rem",
Expand All @@ -458,7 +489,7 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
}

const statusStyle = {
fontSize: "0.8rem",
fontSize: "0.8rem",
padding: "5px",
borderTop: "0.5px solid",
}
Expand All @@ -475,18 +506,18 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
width: "100%"
};
footerStyle;

///////////////////////////////////////////////////////////////////

///////////////////////// HTML PANEL //////////////////////////////

return (
<body>
<div style={{ padding: "1rem",
scrollBehavior: "smooth",
maxHeight:"calc(100% - 25px)",
<div style={{ padding: "1rem",
scrollBehavior: "smooth",
maxHeight:"calc(100% - 25px)",
overflowY: "scroll",
fontFamily: "helvetica",
fontFamily: "helvetica",
fontSize: "1rem",
}}>
<h1>ROS2 Parameter Extension</h1>
Expand All @@ -503,19 +534,19 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
</select>

<form>
<button
style={setButtonStyle}
onMouseEnter={() => setBgColor("#8f8f8f")}
onMouseLeave={() => colorScheme == "dark" ? setBgColor("#4d4d4d"): setBgColor("#d6d6d6")}
onClick={setParam}
<button
style={setButtonStyle}
onMouseEnter={() => setBgColor("#8f8f8f")}
onMouseLeave={() => colorScheme == "dark" ? setBgColor("#4d4d4d"): setBgColor("#d6d6d6")}
onClick={setParam}
type="reset">
Set Parameters
</button>

<label
style={loadButtonStyle}
onMouseEnter={() => setLoadButtonBgColor("#8f8f8f")}
onMouseLeave={() => colorScheme == "dark" ? setLoadButtonBgColor("#4d4d4d"): setLoadButtonBgColor("#d6d6d6")}
<label
style={loadButtonStyle}
onMouseEnter={() => setLoadButtonBgColor("#8f8f8f")}
onMouseLeave={() => colorScheme == "dark" ? setLoadButtonBgColor("#4d4d4d"): setLoadButtonBgColor("#d6d6d6")}
>
<input type="file" style={{display: "none"}} onChange={(event) => {loadFile(event.target.files)}}/>
Load
Expand All @@ -532,15 +563,15 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem
<b style={{ borderBottom: "1px solid", padding: "2px", marginBottom: "3px" }}>Type</b>
<b style={{ borderBottom: "1px solid", padding: "2px", marginBottom: "3px" }}>Value</b>
<b style={{ borderBottom: "1px solid", padding: "2px", marginBottom: "3px" }}>New Value</b>

{(paramList ?? []).map((result) => (
<>
<div style={{margin: "0px 4px 0px 4px"}} key={result.name}>{result.name}:</div>
<div style={{margin: "0px 4px 0px 4px"}}>{getType(result.value)}</div>
<div style={{margin: "0px 4px 0px 4px"}}>{getParameterValue(result.value)}</div>
<div style={{margin: "0px 4px 0px 4px"}}>
<div style={{margin: "0px 4px 0px 4px"}}>
{createInputBox(result)}
</div>
</div>
</>
))}
</div>
Expand All @@ -558,4 +589,4 @@ function ExamplePanel({ context }: { context: PanelExtensionContext }): JSX.Elem

export function initParamsPanel(context: PanelExtensionContext) {
ReactDOM.render(<ExamplePanel context={context} />, context.panelElement);
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ExtensionContext } from "@foxglove/studio";
import { initParamsPanel } from "./ExamplePanel";

export function activate(extensionContext: ExtensionContext) {
extensionContext.registerPanel({ name: "Custom Parameters Extenstion", initPanel: initParamsPanel });
extensionContext.registerPanel({ name: "Custom Parameters Extension", initPanel: initParamsPanel });
}