Skip to content

Commit 9bc9004

Browse files
authored
add user script (#340)
1 parent aceee99 commit 9bc9004

File tree

4 files changed

+531
-2
lines changed

4 files changed

+531
-2
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
---
2+
sidebar_position: 10
3+
---
4+
5+
# 用户脚本面板
6+
7+
用户脚本面板支持用户编写自定义脚本(使用 TypeScript),对输入消息进行转换并输出到新的主题(topic)。该功能支持对回放数据和范围加载数据进行处理,适用于快速数据转换与调试。
8+
9+
- 回放数据:逐帧流式传入的消息,例如[原始消息面板](./9-raw-messages.md)[三维面板](./2-3d-panel.md)的数据。
10+
- 范围加载数据:一次性加载整个回放范围内的消息,例如[图表面板](./4-plot-panel.md)或状态转换面板的数据。
11+
12+
注意:用户脚本仅作用于当前布局。若需要在所有布局中统一转换消息,请使用[消息转换器](../8-extensions/1-introduction.md#message-converters)
13+
14+
## 快速开始
15+
用户脚本使用 TypeScript 编写。
16+
> **提示**
17+
>
18+
> TypeScript 是 JavaScript 的超集,因此可通过 JavaScript 术语搜索语法问题(如操作数组或访问对象属性),通过 TypeScript 术语搜索语义问题(如设置可选对象属性)。
19+
20+
### 编写第一个脚本
21+
每个脚本必须声明以下 3 个导出项:
22+
23+
- `inputs` - 待转换的输入 topic 数组
24+
- `output` - 转换后的输出 topic 名称
25+
- `script` - 处理输入消息并发布到输出 topic 的函数(必须是[默认导出](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#description)
26+
27+
示例脚本(将 `/rosout` 消息原样输出到 `/coscene_script/echo`):
28+
29+
```typescript
30+
import { Input, Message } from "./types";
31+
32+
33+
export const inputs = ["/rosout"];
34+
export const output = "/coscene_script/echo";
35+
36+
37+
export default function script(event: Input<"/rosout">): Message<"rosgraph_msgs/Log"> {
38+
return event.message;
39+
}
40+
```
41+
42+
若可视化中包含 `/rosout` topic,则可在[原始消息面板](./9-raw-messages.md)中查看 `/studio_script/echo` topic。
43+
44+
当创建一个新脚本时,系统会自动生成示例模板代码:
45+
46+
```typescript
47+
import { Input, Message } from "./types";
48+
49+
type Output = { hello: string };
50+
51+
export const inputs = ["/input/topic"];
52+
export const output = "/studio_script/output_topic";
53+
54+
export default function script(event: Input<"/input/topic">): Output {
55+
return { hello: "world!" };
56+
}
57+
```
58+
59+
其中:
60+
61+
- `Input``Message` 类型是从 `./types` 模块中导入的,该模块为输入事件和消息提供了辅助类型。
62+
- `Output` 类型包含一些默认属性,脚本函数的输出必须符合这些属性要求。
63+
- `Input` 是一个泛型类型,需要传入参数才能使用。这里故意留空,你需要填入输入 topic 的名称,例如:`Input<"/rosout">`
64+
- 输入 `event` 为只读。请勿修改该 `event` 对象。
65+
66+
关于 **Output** 类型,你有两种方式:
67+
68+
* 手动定义你关心的输出属性(即模板代码里提供的那些属性);
69+
* 或者使用上面引入的 **Message** 类型中动态生成的类型。例如,如果你想发布一个 marker 数组,可以返回 `Message<"visualization_msgs/MarkerArray">` 类型。
70+
71+
需要注意的是,消息属性对可视化结果的影响并不总是直观可见。通过严格类型约束,你可以在编译时发现问题,而不是等到运行时才暴露。
72+
73+
当然,在写脚本草稿时,如果你不想被 Typescript 校验打断,可以在想忽略的那行代码前加上 `// @ts-expect-error` 来关闭类型检查。
74+
75+
### 使用多输入 topic
76+
通过联合类型处理多个输入 topic 的消息:
77+
78+
```typescript
79+
import { Input, Message } from "./types";
80+
81+
export const inputs = ["/rosout", "/tf"];
82+
export const output = "/coscene_script/echo";
83+
84+
export default function script(event: Input<"/rosout"> | Input<"/tf">): { data: number[] } {
85+
if (event.topic === "/rosout") {
86+
// read event.message fields expected for /rosout messages
87+
} else {
88+
// read event.message fields expected for /tf messages
89+
}
90+
91+
return { data: [] };
92+
}
93+
```
94+
95+
这段代码片段使用了联合类型(union types),用来声明脚本函数中的消息既可以来自 `/rosout` topic,也可以来自 `/tf` topic。处理消息时,可以通过 `if/else` 判断不同的 schema 名称,从而区分具体是哪个 topic 的消息。
96+
97+
如果你需要合并多个 topic 的消息,可以在脚本的全局作用域中创建一个变量,并在每次脚本函数被调用时引用它。同时要检查时间戳,确保不会发布不同步的数据。
98+
99+
```typescript
100+
import { Input, Message, Time } from "./types";
101+
102+
export const inputs = ["/rosout", "/tf"];
103+
export const output = "/coscene_script/echo";
104+
105+
let lastReceiveTime: Time = { sec: 0, nsec: 0 };
106+
const myScope: { tf?: Message<"tf2_msgs/TFMessage">; rosout?: Message<"rosgraph_msgs/Log"> } = {};
107+
108+
export default function script(
109+
event: Input<"/rosout"> | Input<"/tf">,
110+
): { data: number[] } | undefined {
111+
const { receiveTime } = message;
112+
let inSync = true;
113+
114+
if (receiveTime.sec !== lastReceiveTime.sec || receiveTime.nsec !== lastReceiveTime.nsec) {
115+
lastReceiveTime = receiveTime;
116+
inSync = false;
117+
}
118+
119+
if (message.topic === "/rosout") {
120+
myScope.rosout = event.message;
121+
} else {
122+
myScope.tf = event.message;
123+
}
124+
125+
if (!inSync) {
126+
return { data: [] };
127+
}
128+
}
129+
```
130+
131+
### 使用全局变量
132+
脚本函数在每次执行时,都会以对象的形式接收所有变量。每当有新消息到来,脚本函数都会用最新的变量值重新运行。
133+
134+
> **注意**
135+
>
136+
> 用户脚本中的全局变量是只读的,请勿修改 `globalVars` 参数。
137+
138+
139+
```typescript
140+
import { Input, Message } from "./types";
141+
142+
type Output = {};
143+
type GlobalVariables = { someNumericaVar: number };
144+
145+
export const inputs = [];
146+
export const output = "/coscene_script/";
147+
148+
export default function script(event: Input<"/foo_marker">, globalVars: GlobalVariables): Output {
149+
if (event.message.id === globalVars.someNumericaVar) {
150+
// Message's id matches $someNumericaVar
151+
}
152+
153+
return { data: [] };
154+
}
155+
```
156+
157+
### 调试
158+
用户脚本只有在布局中有使用其输出 topic 时才会被执行。
159+
160+
要调试脚本,先在布局中添加一个订阅输出 topic 的原始消息面板。然后,你可以直接查看输入 topic 的消息,或者在脚本中使用 `log(someValue)` 将值打印到面板底部的 Logs 区域。
161+
162+
唯一不能使用 `log()` 打印的值是函数本身,或者包含函数定义的值。你也可以一次打印多个值,例如:`log(someValue, anotherValue, yetAnotherValue)`
163+
164+
以下 log 语句不会产生任何错误:
165+
166+
```typescript
167+
const addNums = (a: number, b: number): number => a + b;
168+
log(50, "ABC", null, undefined, { abc: 2, def: false });
169+
log(1 + 2, addNums(1, 2));
170+
```
171+
172+
但包含函数定义的值会报错:
173+
174+
```typescript
175+
log(() => {});
176+
log(addNums);
177+
log({ subtractNums: (a: number, b: number): number => a - b });
178+
```
179+
180+
在脚本函数外调用 `log()` 会在脚本注册时执行一次;在脚本函数内部调用 `log()`,则会在每次脚本函数被调用时打印该值。
181+
182+
> **注意**
183+
>
184+
> 对于高频发布的 topic,使用 `log()` 可能会降低用户脚本的执行效率。
185+
>
186+
> 此外,由于图表面板会对渲染时间范围内的所有消息调用用户脚本,当在图表面板中查看用户脚本输出时,`log()` 的内容不会显示。这种情况下,可以使用原始消息面板来查看输出消息。
187+
188+
### 跳过输出
189+
当你不希望发布消息时,可以在函数体内提前(或延迟)返回。例如,假设你只想在输入中的某个常量不等于 3 时才发布消息:
190+
191+
```typescript
192+
import { Input } from "./types";
193+
194+
export const inputs = ["/state"];
195+
export const output = "/coscene_script/manual_metrics";
196+
197+
export default function script(event: Input<"/state">): { metrics: number } | undefined {
198+
if (event.message.constant === 3) {
199+
// Do not publish any message
200+
return;
201+
}
202+
return {
203+
// Your data here
204+
};
205+
}
206+
```
207+
208+
在 TypeScript 中,如果你直接 `return` 而不带返回值,函数会隐式返回 `undefined`。请注意脚本函数的联合返回类型——我们已经告诉 TypeScript,该函数可能返回 `undefined`
209+
210+
### 使用 @foxglove/schemas
211+
在用户脚本中,可以从 [@foxglove/schemas](https://github.com/foxglove/foxglove-sdk) 包导入并使用类型:
212+
213+
```typescript
214+
import { Input } from "./types";
215+
import { Color } from "@foxglove/schemas";
216+
217+
export const inputs = ["/imu"];
218+
export const output = "/s_script/json_data";
219+
220+
export default function script(event: Input<"/imu">): Color {
221+
return {
222+
r: 1,
223+
g: 1,
224+
b: 1,
225+
a: 1,
226+
};
227+
}
228+
```
229+
230+
## 工具与模板
231+
232+
- **Utilities 标签页**:包含可在任意脚本中导入使用的函数(例如:`import { compare } from "./time.ts"`)。`types.ts` 工具文件会根据当前加载的数据源生成,包含所有已发现 schema 的类型定义。
233+
- **Templates 标签页**:包含常见脚本模板,如发布 `MarkerArray` 的脚本
234+
235+
## 设置
236+
237+
| 通用 | |
238+
| --- | --- |
239+
| 保存时自动格式化 | 保存时自动格式化脚本中的代码 |
240+
241+
## 快捷键
242+
输入 `Cmd` + `S` 保存脚本
243+
244+
## TypeScript 资源
245+
246+
- [Basic Types](https://www.typescriptlang.org/docs/handbook/2/basic-types.html)
247+
- [Gitbook](https://basarat.gitbook.io/typescript/getting-started/why-typescript)
248+
249+
## 用户脚本 vs Topic Converter 扩展
250+
用户脚本和 [topic converter 扩展](../8-extensions/1-introduction.md#message-converters)功能相似,但在编写方式、共享方式以及对第三方包的支持上存在关键区别。
251+
252+
| 功能 | 用户脚本 | Topic Converter 扩展 |
253+
| --- | --- | --- |
254+
| 数据转换 |||
255+
| 创建新 topic |||
256+
| 直接编辑 |||
257+
| 作用于一个布局 |||
258+
| 跨布局复用 |||
259+
| 团队共享 |||
260+
| 在你的 IDE 中编辑 |||
261+
| 你代码库的一部分 |||
262+
| 使用第三方包 |||

docs/viz/8-extensions/1-introduction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ sidebar_position: 1
2121
* 指南:创建自定义面板
2222
* 构建自定义面板扩展(React)
2323

24-
## 消息转换器
24+
## 消息转换器 {#message-converters}
2525

2626
消息转换器扩展允许您将消息从一种架构转换为另一种架构。通过将消息转换为符合可视化支持的架构,您可以使用可视化的内置可视化功能检查它们。例如,您可以使用消息转换器将自定义 GPS 消息转换为 foxglove.LocationFix 消息,以便在地图面板中可视化。
2727

0 commit comments

Comments
 (0)