|
| 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 | +| 使用第三方包 | ❌ | ✅ | |
0 commit comments