From d4b4447887443d284bdb67cc6897574ff3f47fa3 Mon Sep 17 00:00:00 2001 From: "xiran.yu" Date: Thu, 18 Sep 2025 15:10:37 +0800 Subject: [PATCH] add user script --- docs/viz/4-panel/10-user-scripts.md | 262 +++++++++++++++++ docs/viz/8-extensions/1-introduction.md | 2 +- .../current/viz/4-panel/10-user-scripts.md | 267 ++++++++++++++++++ .../viz/8-extensions/1-introduction.md | 2 +- 4 files changed, 531 insertions(+), 2 deletions(-) create mode 100644 docs/viz/4-panel/10-user-scripts.md create mode 100644 i18n/en/docusaurus-plugin-content-docs/current/viz/4-panel/10-user-scripts.md diff --git a/docs/viz/4-panel/10-user-scripts.md b/docs/viz/4-panel/10-user-scripts.md new file mode 100644 index 000000000..b21226463 --- /dev/null +++ b/docs/viz/4-panel/10-user-scripts.md @@ -0,0 +1,262 @@ +--- +sidebar_position: 10 +--- + +# 用户脚本面板 + +用户脚本面板支持用户编写自定义脚本(使用 TypeScript),对输入消息进行转换并输出到新的主题(topic)。该功能支持对回放数据和范围加载数据进行处理,适用于快速数据转换与调试。 + +- 回放数据:逐帧流式传入的消息,例如[原始消息面板](./9-raw-messages.md)或 [三维面板](./2-3d-panel.md)的数据。 +- 范围加载数据:一次性加载整个回放范围内的消息,例如[图表面板](./4-plot-panel.md)或状态转换面板的数据。 + +注意:用户脚本仅作用于当前布局。若需要在所有布局中统一转换消息,请使用[消息转换器](../8-extensions/1-introduction.md#message-converters)。 + +## 快速开始 +用户脚本使用 TypeScript 编写。 +> **提示** +> +> TypeScript 是 JavaScript 的超集,因此可通过 JavaScript 术语搜索语法问题(如操作数组或访问对象属性),通过 TypeScript 术语搜索语义问题(如设置可选对象属性)。 + +### 编写第一个脚本 +每个脚本必须声明以下 3 个导出项: + +- `inputs` - 待转换的输入 topic 数组 +- `output` - 转换后的输出 topic 名称 +- `script` - 处理输入消息并发布到输出 topic 的函数(必须是[默认导出](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#description)) + +示例脚本(将 `/rosout` 消息原样输出到 `/coscene_script/echo`): + +```typescript +import { Input, Message } from "./types"; + + +export const inputs = ["/rosout"]; +export const output = "/coscene_script/echo"; + + +export default function script(event: Input<"/rosout">): Message<"rosgraph_msgs/Log"> { + return event.message; +} +``` + +若可视化中包含 `/rosout` topic,则可在[原始消息面板](./9-raw-messages.md)中查看 `/studio_script/echo` topic。 + +当创建一个新脚本时,系统会自动生成示例模板代码: + +```typescript +import { Input, Message } from "./types"; + +type Output = { hello: string }; + +export const inputs = ["/input/topic"]; +export const output = "/studio_script/output_topic"; + +export default function script(event: Input<"/input/topic">): Output { + return { hello: "world!" }; +} +``` + +其中: + +- `Input` 和 `Message` 类型是从 `./types` 模块中导入的,该模块为输入事件和消息提供了辅助类型。 +- `Output` 类型包含一些默认属性,脚本函数的输出必须符合这些属性要求。 +- `Input` 是一个泛型类型,需要传入参数才能使用。这里故意留空,你需要填入输入 topic 的名称,例如:`Input<"/rosout">`。 +- 输入 `event` 为只读。请勿修改该 `event` 对象。 + +关于 **Output** 类型,你有两种方式: + +* 手动定义你关心的输出属性(即模板代码里提供的那些属性); +* 或者使用上面引入的 **Message** 类型中动态生成的类型。例如,如果你想发布一个 marker 数组,可以返回 `Message<"visualization_msgs/MarkerArray">` 类型。 + +需要注意的是,消息属性对可视化结果的影响并不总是直观可见。通过严格类型约束,你可以在编译时发现问题,而不是等到运行时才暴露。 + +当然,在写脚本草稿时,如果你不想被 Typescript 校验打断,可以在想忽略的那行代码前加上 `// @ts-expect-error` 来关闭类型检查。 + +### 使用多输入 topic +通过联合类型处理多个输入 topic 的消息: + +```typescript +import { Input, Message } from "./types"; + +export const inputs = ["/rosout", "/tf"]; +export const output = "/coscene_script/echo"; + +export default function script(event: Input<"/rosout"> | Input<"/tf">): { data: number[] } { + if (event.topic === "/rosout") { + // read event.message fields expected for /rosout messages + } else { + // read event.message fields expected for /tf messages + } + + return { data: [] }; +} +``` + +这段代码片段使用了联合类型(union types),用来声明脚本函数中的消息既可以来自 `/rosout` topic,也可以来自 `/tf` topic。处理消息时,可以通过 `if/else` 判断不同的 schema 名称,从而区分具体是哪个 topic 的消息。 + +如果你需要合并多个 topic 的消息,可以在脚本的全局作用域中创建一个变量,并在每次脚本函数被调用时引用它。同时要检查时间戳,确保不会发布不同步的数据。 + +```typescript +import { Input, Message, Time } from "./types"; + +export const inputs = ["/rosout", "/tf"]; +export const output = "/coscene_script/echo"; + +let lastReceiveTime: Time = { sec: 0, nsec: 0 }; +const myScope: { tf?: Message<"tf2_msgs/TFMessage">; rosout?: Message<"rosgraph_msgs/Log"> } = {}; + +export default function script( + event: Input<"/rosout"> | Input<"/tf">, +): { data: number[] } | undefined { + const { receiveTime } = message; + let inSync = true; + + if (receiveTime.sec !== lastReceiveTime.sec || receiveTime.nsec !== lastReceiveTime.nsec) { + lastReceiveTime = receiveTime; + inSync = false; + } + + if (message.topic === "/rosout") { + myScope.rosout = event.message; + } else { + myScope.tf = event.message; + } + + if (!inSync) { + return { data: [] }; + } +} +``` + +### 使用全局变量 +脚本函数在每次执行时,都会以对象的形式接收所有变量。每当有新消息到来,脚本函数都会用最新的变量值重新运行。 + +> **注意** +> +> 用户脚本中的全局变量是只读的,请勿修改 `globalVars` 参数。 + + +```typescript +import { Input, Message } from "./types"; + +type Output = {}; +type GlobalVariables = { someNumericaVar: number }; + +export const inputs = []; +export const output = "/coscene_script/"; + +export default function script(event: Input<"/foo_marker">, globalVars: GlobalVariables): Output { + if (event.message.id === globalVars.someNumericaVar) { + // Message's id matches $someNumericaVar + } + + return { data: [] }; +} +``` + +### 调试 +用户脚本只有在布局中有使用其输出 topic 时才会被执行。 + +要调试脚本,先在布局中添加一个订阅输出 topic 的原始消息面板。然后,你可以直接查看输入 topic 的消息,或者在脚本中使用 `log(someValue)` 将值打印到面板底部的 Logs 区域。 + +唯一不能使用 `log()` 打印的值是函数本身,或者包含函数定义的值。你也可以一次打印多个值,例如:`log(someValue, anotherValue, yetAnotherValue)`。 + +以下 log 语句不会产生任何错误: + +```typescript +const addNums = (a: number, b: number): number => a + b; +log(50, "ABC", null, undefined, { abc: 2, def: false }); +log(1 + 2, addNums(1, 2)); +``` + +但包含函数定义的值会报错: + +```typescript +log(() => {}); +log(addNums); +log({ subtractNums: (a: number, b: number): number => a - b }); +``` + +在脚本函数外调用 `log()` 会在脚本注册时执行一次;在脚本函数内部调用 `log()`,则会在每次脚本函数被调用时打印该值。 + +> **注意** +> +> 对于高频发布的 topic,使用 `log()` 可能会降低用户脚本的执行效率。 +> +> 此外,由于图表面板会对渲染时间范围内的所有消息调用用户脚本,当在图表面板中查看用户脚本输出时,`log()` 的内容不会显示。这种情况下,可以使用原始消息面板来查看输出消息。 + +### 跳过输出 +当你不希望发布消息时,可以在函数体内提前(或延迟)返回。例如,假设你只想在输入中的某个常量不等于 3 时才发布消息: + +```typescript +import { Input } from "./types"; + +export const inputs = ["/state"]; +export const output = "/coscene_script/manual_metrics"; + +export default function script(event: Input<"/state">): { metrics: number } | undefined { + if (event.message.constant === 3) { + // Do not publish any message + return; + } + return { + // Your data here + }; +} +``` + +在 TypeScript 中,如果你直接 `return` 而不带返回值,函数会隐式返回 `undefined`。请注意脚本函数的联合返回类型——我们已经告诉 TypeScript,该函数可能返回 `undefined`。 + +### 使用 @foxglove/schemas +在用户脚本中,可以从 [@foxglove/schemas](https://github.com/foxglove/foxglove-sdk) 包导入并使用类型: + +```typescript +import { Input } from "./types"; +import { Color } from "@foxglove/schemas"; + +export const inputs = ["/imu"]; +export const output = "/s_script/json_data"; + +export default function script(event: Input<"/imu">): Color { + return { + r: 1, + g: 1, + b: 1, + a: 1, + }; +} +``` + +## 工具与模板 + +- **Utilities 标签页**:包含可在任意脚本中导入使用的函数(例如:`import { compare } from "./time.ts"`)。`types.ts` 工具文件会根据当前加载的数据源生成,包含所有已发现 schema 的类型定义。 +- **Templates 标签页**:包含常见脚本模板,如发布 `MarkerArray` 的脚本 + +## 设置 + +| 通用 | | +| --- | --- | +| 保存时自动格式化 | 保存时自动格式化脚本中的代码 | + +## 快捷键 +输入 `Cmd` + `S` 保存脚本 + +## TypeScript 资源 + +- [Basic Types](https://www.typescriptlang.org/docs/handbook/2/basic-types.html) +- [Gitbook](https://basarat.gitbook.io/typescript/getting-started/why-typescript) + +## 用户脚本 vs Topic Converter 扩展 +用户脚本和 [topic converter 扩展](../8-extensions/1-introduction.md#message-converters)功能相似,但在编写方式、共享方式以及对第三方包的支持上存在关键区别。 + +| 功能 | 用户脚本 | Topic Converter 扩展 | +| --- | --- | --- | +| 数据转换 | ✅ | ✅ | +| 创建新 topic | ✅ | ✅ | +| 直接编辑 | ✅ | ❌ | +| 作用于一个布局 | ✅ | ❌ | +| 跨布局复用 | ❌ | ✅ | +| 团队共享 | ❌ | ✅ | +| 在你的 IDE 中编辑 | ❌ | ✅ | +| 你代码库的一部分 | ❌ | ✅ | +| 使用第三方包 | ❌ | ✅ | diff --git a/docs/viz/8-extensions/1-introduction.md b/docs/viz/8-extensions/1-introduction.md index 821c17235..30b69917b 100644 --- a/docs/viz/8-extensions/1-introduction.md +++ b/docs/viz/8-extensions/1-introduction.md @@ -21,7 +21,7 @@ sidebar_position: 1 * 指南:创建自定义面板 * 构建自定义面板扩展(React) -## 消息转换器 +## 消息转换器 {#message-converters} 消息转换器扩展允许您将消息从一种架构转换为另一种架构。通过将消息转换为符合可视化支持的架构,您可以使用可视化的内置可视化功能检查它们。例如,您可以使用消息转换器将自定义 GPS 消息转换为 foxglove.LocationFix 消息,以便在地图面板中可视化。 diff --git a/i18n/en/docusaurus-plugin-content-docs/current/viz/4-panel/10-user-scripts.md b/i18n/en/docusaurus-plugin-content-docs/current/viz/4-panel/10-user-scripts.md new file mode 100644 index 000000000..f815ff0f7 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/viz/4-panel/10-user-scripts.md @@ -0,0 +1,267 @@ +--- +sidebar_position: 10 +--- + +# User Scripts panel + +User scripts are custom in-app scripts (written in TypeScript) that transform messages. A user script can transform both playback and range-loaded. The result is output to a new topic. + +- Playback data – Messages streaming frame-by-frame into Foxglove; e.g. data for the [Raw Messages](./9-raw-messages.md) or [3D panel](./2-3d-panel.md) +- Range-loaded data – Messages for the entire data range being played back; e.g. data for the [Plot](./4-plot-panel.md) or State Transitions panels + +When transforming range-loaded data, Foxglove creates two instances of the running user script – one handles the full data range, while the other handles just the current playback frame of messages. Each instance of the user script receives the messages in log time order. + +> **Tip** +> +> User scripts are local to a layout. Use a [message converter](../8-extensions/1-introduction.md#message-converters) to transform messages in a way that will apply to all layouts. + +## Getting started +User Scripts are written in TypeScript. + +> **Info** +> +> TypeScript is a superset of JavaScript, so you can Google syntactic questions (e.g. how to manipulate arrays, or access object properties) using JavaScript terms, and semantic questions (e.g. how to make an object property optional) using TypeScript terms. + +### Writing your first script +Every script must declare 3 exports: + +- `inputs` – An array of input topics to transform +- `output` – Name of the transformed output topic +- `script` – A function that takes messages from input topics, transforms them, and then publishes messages on the output topic; must be the [default export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#description) + +Here is a basic script that echoes its input on a new output topic, `/coscene_script/echo`: + +```typescript +import { Input, Message } from "./types"; + + +export const inputs = ["/rosout"]; +export const output = "/coscene_script/echo"; + + +export default function script(event: Input<"/rosout">): Message<"rosgraph_msgs/Log"> { + return event.message; +} +``` + +If you open a recording with a `/rosout` topic, you can now inspect the `/studio_script/echo` topic in the [Raw Messages panel](./9-raw-messages.md). + +When you create a new script, you’ll be presented with some boilerplate: + +```typescript +import { Input, Message } from "./types"; + +type Output = { hello: string }; + +export const inputs = ["/input/topic"]; +export const output = "/studio_script/output_topic"; + +export default function script(event: Input<"/input/topic">): Output { + return { hello: "world!" }; +} +``` + +You’ll notice a few things: + +- The `Input` and `Message` types are imported from the `./types` module, which provides helper types for your Input events and messages +- The `Output` type has some default properties that the `script` function's output must adhere to + +`Input` is a generic type, meaning that it takes a parameter in order to be used. It is left empty on purpose as you'll need to populate it with the name of your input topic, e.g. `Input<"/rosout">`. + +> **Note** +> +> The input event is read-only. Do not modify the event object. + +As for the `Output` type, you can either manually type out your output with the properties you care about (i.e. what is available in the boilerplate) or use one of the dynamically generated types from the `Message` type imported above. For instance, if you want to publish an array of markers, you can return the type `Message<"visualization_msgs/MarkerArray">`. + +It's not always obvious how message properties affect the visualized output – strictly typing your scripts helps you debug issues at compile time rather than at runtime. With that said, you can disable Typescript checks when working on a rough draft of your script by adding `// @ts-expect-error` on the line above the one you want to ignore. + +### Using multiple input topics +In some cases, you will want to define multiple input topics: + +```typescript +import { Input, Message } from "./types"; + +export const inputs = ["/rosout", "/tf"]; +export const output = "/coscene_script/echo"; + +export default function script(event: Input<"/rosout"> | Input<"/tf">): { data: number[] } { + if (event.topic === "/rosout") { + // read event.message fields expected for /rosout messages + } else { + // read event.message fields expected for /tf messages + } + + return { data: [] }; +} +``` + +This snippet uses union types to assert that the message in the `script` function can take either a `/rosout` or `/tf` topic. Use an if/else clause to differentiate between incoming topics' schema names when manipulating messages. + +To combine messages from multiple topics, create a variable in your script's global scope to reference every time your `script` function is invoked. Check timestamps to make sure you are not publishing out-of-sync data. + +```typescript +import { Input, Message, Time } from "./types"; + +export const inputs = ["/rosout", "/tf"]; +export const output = "/coscene_script/echo"; + +let lastReceiveTime: Time = { sec: 0, nsec: 0 }; +const myScope: { tf?: Message<"tf2_msgs/TFMessage">; rosout?: Message<"rosgraph_msgs/Log"> } = {}; + +export default function script( + event: Input<"/rosout"> | Input<"/tf">, +): { data: number[] } | undefined { + const { receiveTime } = message; + let inSync = true; + + if (receiveTime.sec !== lastReceiveTime.sec || receiveTime.nsec !== lastReceiveTime.nsec) { + lastReceiveTime = receiveTime; + inSync = false; + } + + if (message.topic === "/rosout") { + myScope.rosout = event.message; + } else { + myScope.tf = event.message; + } + + if (!inSync) { + return { data: [] }; + } +} +``` + +### Using global variables +The `script` function will receive all of the variables as an object every time it is called. Each time a new message is received, the `script` function will be re-run with the latest variable values: + +> **Note** +> +> Global variables are read-only on user-scripts. Do not modify the globalVars parameter. + +```typescript +import { Input, Message } from "./types"; + +type Output = {}; +type GlobalVariables = { someNumericaVar: number }; + +export const inputs = []; +export const output = "/coscene_script/"; + +export default function script(event: Input<"/foo_marker">, globalVars: GlobalVariables): Output { + if (event.message.id === globalVars.someNumericaVar) { + // Message's id matches $someNumericaVar + } + + return { data: [] }; +} +``` + +### Debugging +User scripts are not executed unless the output topic is being used somewhere within your layout. + +To debug your script, first add a Raw Messages panel subscribing to the output topic to your layout. From there, you can either inspect the incoming topic directly, or invoke `log(someValue)` throughout the user script to print values to the Logs section at the bottom of the panel. + +The only value you cannot `log()` is one that is, or contains, a function definition. You can also log multiple values at once, e.g. `log(someValue, anotherValue, yetAnotherValue)`. + +The following log statements will not produce any errors: + +```typescript +const addNums = (a: number, b: number): number => a + b; +log(50, "ABC", null, undefined, { abc: 2, def: false }); +log(1 + 2, addNums(1, 2)); +``` + +But these containing function definitions will: + +```typescript +log(() => {}); +log(addNums); +log({ subtractNums: (a: number, b: number): number => a - b }); +``` + +Invoking `log()` outside your `script` function will invoke it once, when your script is registered. Invoking `log()` inside your `script` function will log that value every time your `script` function is called. + +> **Tip** +> +> For topics publishing at a high rate, using `log()` can slow down the user script. +> +> Because a Plot panel will invoke the user script across all messages in the rendered time-range, `log()` output is not shown when plotting the output of a user script. In this case, use a Raw Messages panel to view the output message instead. + +### Skipping output +Do an early (or late) `return` in your function body when you don't want to publish. For example, let's say you only wanted to publish messages when a constant in the input is not 3: + +```typescript +import { Input } from "./types"; + +export const inputs = ["/state"]; +export const output = "/coscene_script/manual_metrics"; + +export default function script(event: Input<"/state">): { metrics: number } | undefined { + if (event.message.constant === 3) { + // Do not publish any message + return; + } + return { + // Your data here + }; +} +``` + +In Typescript, if you return without a value, it will implicitly return `undefined`. Note the union return type for the `script` function – we've indicated to Typescript that this function can return `undefined`. + +### Using @foxglove/schemas +Import and use types from the [@foxglove/schemas](https://github.com/foxglove/foxglove-sdk) package in user scripts: + +```typescript +import { Input } from "./types"; +import { Color } from "@foxglove/schemas"; + +export const inputs = ["/imu"]; +export const output = "/s_script/json_data"; + +export default function script(event: Input<"/imu">): Color { + return { + r: 1, + g: 1, + b: 1, + a: 1, + }; +} +``` + +## Utilities and templates +The sidebar's "Utilities" tab includes functions that can be imported for use in any script (e.g. `import { compare } from "./time.ts"`). The `types.ts` utility file is generated from the currently loaded data source, and contains type definitions for all found schemas. + +We currently do not allow importing 3rd-party packages, but let us know if there are packages that would be useful to you! + +The Templates tab includes boilerplate for writing common scripts, like one that publishes a `MarkerArray`. If you have any other use cases that would work well as a template, please let us know. + +## Settings + +| General | | +| --- | --- | +| Auto-format on save | Auto-format the code in your script on save | + +## Controls and shortcuts +Press `Cmd` + `s` to save script changes. + +## TypeScript Resources + +- [Basic Types](https://www.typescriptlang.org/docs/handbook/2/basic-types.html) +- [Gitbook](https://basarat.gitbook.io/typescript/getting-started/why-typescript) + +## When to use a user script or a topic converter extension +User scripts and [topic converter extensions](../8-extensions/1-introduction.md#message-converters) have similar capabilities, but there are key differences in how they are authored, shared, and how they support third-party packages. + +| | User scripts | Topic converters | +| --- | --- | --- | +| Data transformation | ✅ | ✅ | +| Create new topics | ✅ | ✅ | +| Edit directly | ✅ | ❌ | +| Scoped to a layout | ✅ | ❌ | +| Reusable across layouts | ❌ | ✅ | +| Shareable with your team | ❌ | ✅ | +| Written in your IDE | ❌ | ✅ | +| Part of your codebase | ❌ | ✅ | +| Use third-party packages | ❌ | ✅ | \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/viz/8-extensions/1-introduction.md b/i18n/en/docusaurus-plugin-content-docs/current/viz/8-extensions/1-introduction.md index e03aa003f..3b3623f30 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/viz/8-extensions/1-introduction.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/viz/8-extensions/1-introduction.md @@ -21,7 +21,7 @@ Custom panels are ideal when your visualization or interaction requirements are - Guide: Creating Custom Panels - Building Custom Panel Extensions (React) -## Message Converters +## Message Converters {#message-converters} Message converter extensions allow you to transform messages from one schema to another. By converting messages to schemas supported by visualization, you can inspect them using visualization's built-in visualization features. For example, you can use a message converter to transform custom GPS messages into foxglove.LocationFix messages for visualization in the map panel.