Skip to content

feat(simulation): bind Hub models at runtime - #18

Merged
Mile-Away merged 1 commit into
mainfrom
codex/simulation-hub-model-binding
Sep 26, 2026
Merged

Mile-Away merged 1 commit into
mainfrom
codex/simulation-hub-model-binding

Conversation

@Mile-Away

@Mile-Away Mile-Away commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Virtual devices can now bind a verified Hub model at runtime through the canonical set_asset and clear_asset actions. The selected binding is emitted in telemetry as simulation_asset, so clients can keep the rendered model synchronized with the simulation process.

Validation

  • cargo test -p osdl-core --features espnow
  • 122 unit tests, 6 MQTT E2E tests, and 18 integration tests passed

Sourcery 摘要

启用确定性的本地虚拟实验室,并通过运行时资产绑定同步其可视化 Hub 模型。

新功能:

  • 添加确定性的本地模拟模式,使用标准的设备、操作、遥测、事件和 gRPC 契约支持虚拟加热器、搅拌器、阀门和传感器设备。
  • 通过 set_asset 和 clear_asset 支持虚拟设备的运行时 Hub 模型绑定与清除,并将选定的身份以 simulation_asset 遥测数据的形式公开。
  • 提供可扩展的模拟后端接口和配置,支持世界身份、引擎、刻度频率、种子、设备、属性、操作以及可选资产引用。

增强功能:

  • 通过实验室 CLI 和环境配置公开模拟启动方式,并提供有文档说明的自定义世界配置方案。
  • 增加对模拟设备注册、遥测、操作和资产生命周期变更的集成测试与传输层覆盖。

文档:

  • 记录本地模拟启动、自定义世界配置以及 Hub 模型绑定行为。

测试:

  • 添加涵盖模拟配置验证、确定性遥测和操作处理、运行时资产绑定与清除,以及端到端引擎集成的测试。
Original summary in English

Sourcery 摘要

支持模拟设备在运行时绑定 Hub 模型,并通过遥测同步所选的视觉标识。

新功能:

  • 支持虚拟设备在运行时通过规范操作绑定或清除 Hub 模型标识。
  • 通过模拟遥测公开当前的运行时模型绑定,以便客户端同步。

增强功能:

  • 验证必需的资产标识字段,并拒绝运行时绑定中的空版本。

文档:

  • 记录运行时模型绑定行为,以及其与持久化资产配置之间的区别。

测试:

  • 增加对绑定和清除 Hub 资产的测试,并验证相应的遥测更新。
Original summary in English

Summary by Sourcery

Support runtime Hub model binding for simulated devices and synchronize the selected visual identity through telemetry.

New Features:

  • Enable virtual devices to bind or clear Hub model identities at runtime through canonical actions.
  • Expose the active runtime model binding through simulation telemetry for synchronized clients.

Enhancements:

  • Validate required asset identity fields and reject empty versions for runtime bindings.

Documentation:

  • Document runtime model binding behavior and its distinction from persistent asset configuration.

Tests:

  • Add coverage for binding and clearing a Hub asset and verifying the corresponding telemetry updates.

@sourcery-ai

sourcery-ai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

审查者指南

此 PR 引入了一个可选择启用的确定性模拟运行时,通过 OpenSDL 现有的设备和事件契约公开虚拟设备,并支持可扩展的后端以及 CLI/配置接入。同时,它还通过 set_asset 和 clear_asset 实现了运行时 Hub 模型绑定,将所选标识作为 simulation_asset telemetry 传播给同步客户端,并新增了测试和本地开发文档。

运行时 Hub 资产绑定时序图

sequenceDiagram
    participant Client
    participant Engine
    participant Adapter as SimulationAdapter
    participant Transport as SimulationTransport
    participant Backend as KinematicBackend

    Client->>Engine: send DeviceCommand
    Engine->>Adapter: encode_command
    Adapter->>Transport: send
    Transport->>Backend: apply_action
    Backend-->>Transport: update simulation_asset
    Transport-->>Engine: telemetry envelope
    Engine->>Adapter: decode_response
    Adapter-->>Client: simulation_asset telemetry
Loading

模拟资产同步流程图

flowchart TD
    Start[Virtual device starts] --> Snapshot[Telemetry snapshot emitted]
    Snapshot --> Action{set_asset or clear_asset}
    Action --> Set[set_asset validates namespace and name]
    Action --> Clear[clear_asset removes simulation_asset]
    Set --> Telemetry[Next telemetry includes simulation_asset]
    Clear --> Telemetry
    Telemetry --> Client[UI resolves or removes Hub model]
Loading

文件级变更

变更 详情 文件
添加可配置的确定性模拟世界,使用标准的设备、传输、适配器、命令和 telemetry 路径。
  • 引入模拟世界/设备配置,支持验证、默认值、带种子的元数据、操作 schema、位置以及可选的资产引用。
  • 实现运动学后端和基于 tick 驱动的模拟传输,支持虚拟设备注册、telemetry 发送、状态演化以及可扩展的后端注入。
  • 在引擎启动和关闭期间注册并正常停止已配置的模拟设备,并通过 lab CLI 暴露模拟模式。
  • 添加模拟协议适配器,用于编码命令并将 telemetry 信封解码为标准设备属性。
crates/osdl-core/src/config.rs
crates/osdl-core/src/transport/simulation.rs
crates/osdl-core/src/adapter/simulation.rs
crates/osdl-core/src/engine.rs
crates/osdl-core/src/transport/mod.rs
crates/osdl-core/src/adapter/mod.rs
crates/osdl-core/src/lib.rs
crates/lab-cli/src/commands/serve.rs
通过规范化的操作和 telemetry,为虚拟设备实现运行时 Hub 模型绑定。
  • 为默认虚拟设备 schema 添加 set_asset 和 clear_asset 操作。
  • 验证资产标识符,在后端状态中更新或移除 simulation_asset,并在 telemetry 中发送结果。
  • 支持初始 asset_ref 配置,以实现持久绑定,同时不复制 Hub 模型数据。
crates/osdl-core/src/config.rs
crates/osdl-core/src/transport/simulation.rs
docs/recipes/simulation-mode.md
docs/recipes/configs/simulation.yaml
验证模拟契约,并记录本地开发工作流。
  • 添加传输测试,覆盖 telemetry、设备操作以及资产绑定/清除行为。
  • 添加引擎集成测试,确认虚拟设备注册、命令分发和状态事件。
  • 记录 CLI 启动、自定义世界、后端选择以及 Hub 资产解析行为。
crates/osdl-core/src/transport/simulation.rs
crates/osdl-core/tests/integration.rs
docs/recipes/simulation-mode.md
docs/recipes/configs/simulation.yaml

提示和命令

与 Sourcery 交互

  • 触发新的审查: 在 pull request 中评论 @sourcery-ai review。
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。你也可以回复审查评论并发送 @sourcery-ai issue,以从该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title,以随时生成或重新生成标题。
  • 生成 pull request 摘要: 在 pull request 正文的任意位置写入 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary,以随时生成或重新生成摘要。
  • 生成审查者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时生成或重新生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理完所有评论并且不想再看到它们,这会很有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 审查。如果你想从新的审查开始,这尤其有用——别忘了评论 @sourcery-ai review 以触发新的审查!

自定义你的体验

访问你的控制面板以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

The PR introduces an opt-in deterministic simulation runtime with virtual devices exposed through OpenSDL’s existing device and event contracts, including extensible backend support and CLI/configuration wiring. It also enables runtime Hub model binding via set_asset and clear_asset, propagating the selected identity as simulation_asset telemetry for synchronized clients, with tests and local-development documentation.

Sequence diagram for runtime Hub asset binding

sequenceDiagram
    participant Client
    participant Engine
    participant Adapter as SimulationAdapter
    participant Transport as SimulationTransport
    participant Backend as KinematicBackend

    Client->>Engine: send DeviceCommand
    Engine->>Adapter: encode_command
    Adapter->>Transport: send
    Transport->>Backend: apply_action
    Backend-->>Transport: update simulation_asset
    Transport-->>Engine: telemetry envelope
    Engine->>Adapter: decode_response
    Adapter-->>Client: simulation_asset telemetry
Loading

Flow diagram for simulation asset synchronization

flowchart TD
    Start[Virtual device starts] --> Snapshot[Telemetry snapshot emitted]
    Snapshot --> Action{set_asset or clear_asset}
    Action --> Set[set_asset validates namespace and name]
    Action --> Clear[clear_asset removes simulation_asset]
    Set --> Telemetry[Next telemetry includes simulation_asset]
    Clear --> Telemetry
    Telemetry --> Client[UI resolves or removes Hub model]
Loading

File-Level Changes

Change Details Files
Adds a configurable deterministic simulation world that uses the standard device, transport, adapter, command, and telemetry paths.
  • Introduces simulation world/device configuration with validation, defaults, seeded metadata, action schemas, positions, and optional asset references.
  • Implements a kinematic backend and tick-driven simulation transport with virtual device registration, telemetry emission, state evolution, and extensible backend injection.
  • Registers and cleanly stops configured simulation devices during engine startup and shutdown, and exposes simulation mode through the lab CLI.
  • Adds the simulation protocol adapter to encode commands and decode telemetry envelopes into standard device properties.
crates/osdl-core/src/config.rs
crates/osdl-core/src/transport/simulation.rs
crates/osdl-core/src/adapter/simulation.rs
crates/osdl-core/src/engine.rs
crates/osdl-core/src/transport/mod.rs
crates/osdl-core/src/adapter/mod.rs
crates/osdl-core/src/lib.rs
crates/lab-cli/src/commands/serve.rs
Implements runtime Hub model binding for virtual devices through canonical actions and telemetry.
  • Adds set_asset and clear_asset actions to default virtual device schemas.
  • Validates asset identifiers, updates or removes simulation_asset in backend state, and emits the result in telemetry.
  • Supports initial asset_ref configuration for durable bindings without copying Hub model data.
crates/osdl-core/src/config.rs
crates/osdl-core/src/transport/simulation.rs
docs/recipes/simulation-mode.md
docs/recipes/configs/simulation.yaml
Verifies the simulation contract and documents local development workflows.
  • Adds transport tests covering telemetry, device actions, and asset bind/clear behavior.
  • Adds an engine integration test confirming virtual device registration, command dispatch, and status events.
  • Documents CLI startup, custom worlds, backend selection, and Hub asset resolution behavior.
crates/osdl-core/src/transport/simulation.rs
crates/osdl-core/tests/integration.rs
docs/recipes/simulation-mode.md
docs/recipes/configs/simulation.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嘿——我发现了 2 个问题

面向 AI Agent 的提示
请处理这次代码审查中的评论:

## 单独评论

### 评论 1
<location path="crates/osdl-core/src/transport/simulation.rs" line_range="332-348" />
<code_context>
+            .and_then(Value::as_str)
+            .ok_or("simulation: command missing action")?;
+        let params = command.get("params").cloned().unwrap_or_else(|| json!({}));
+        if action == "set_asset" {
+            let namespace = params
+                .get("namespace")
+                .and_then(Value::as_str)
+                .unwrap_or("");
+            let name = params.get("name").and_then(Value::as_str).unwrap_or("");
+            if namespace.trim().is_empty() || name.trim().is_empty() {
+                return Err("simulation: set_asset requires namespace and name".into());
+            }
+            if params
+                .get("version")
</code_context>
<issue_to_address>
**问题 (bug_risk):** `set_asset` 接受任意非空的命名空间、名称和可选版本,并在未查询或验证 Hub 模型的情况下将其作为 `simulation_asset` 发出,因此任意或不存在的身份都会被客户端当作已验证的 Hub 绑定。

**触发条件:** 客户端发送了语法有效但不存在、未发布或未经验证的 Hub 身份时。

**建议修复:** 在修改状态之前,通过 Hub 验证/目录边界解析该引用;或者将此操作重命名并记录为接受未经验证的身份。
</issue_to_address>

### 评论 2
<location path="crates/osdl-core/src/transport/simulation.rs" line_range="442-482" />
<code_context>
+        let update = rx.recv().await.expect("action telemetry");
</code_context>
<issue_to_address>
**问题 (testing):** 测试在发送 `set_asset` 后读取下一条遥测消息,并假定它是操作响应;但周期性 tick 任务可能会先发出未更改的遥测快照,导致断言出现非确定性失败。

**触发条件:** 10 Hz 的 tick 在 `send` 和测试的 `rx.recv()` 之间运行时。

**建议修复:** 持续读取遥测消息,直到消息包含预期的资产绑定;或者在断言操作响应时停止 tick 任务或与其协调。
</issue_to_address>

Sourcery 对开源项目免费——如果你喜欢我们的审查,请考虑分享 ✨
Original comment in English

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="crates/osdl-core/src/transport/simulation.rs" line_range="332-348" />
<code_context>
+            .and_then(Value::as_str)
+            .ok_or("simulation: command missing action")?;
+        let params = command.get("params").cloned().unwrap_or_else(|| json!({}));
+        if action == "set_asset" {
+            let namespace = params
+                .get("namespace")
+                .and_then(Value::as_str)
+                .unwrap_or("");
+            let name = params.get("name").and_then(Value::as_str).unwrap_or("");
+            if namespace.trim().is_empty() || name.trim().is_empty() {
+                return Err("simulation: set_asset requires namespace and name".into());
+            }
+            if params
+                .get("version")
</code_context>
<issue_to_address>
**issue (bug_risk):** `set_asset` accepts any non-empty namespace, name, and optional version and emits it as `simulation_asset` without consulting or verifying a Hub model, so arbitrary or nonexistent identities are presented to clients as verified Hub bindings.

**Triggers:** When a client sends a syntactically valid but nonexistent, unpublished, or unverified Hub identity.

**Suggested fix:** Resolve the reference through the Hub verification/catalog boundary before mutating state, or rename/document the action as accepting an unverified identity.
</issue_to_address>

### Comment 2
<location path="crates/osdl-core/src/transport/simulation.rs" line_range="442-482" />
<code_context>
+        let update = rx.recv().await.expect("action telemetry");
</code_context>
<issue_to_address>
**issue (testing):** The test reads the next telemetry message after sending `set_asset` and assumes it is the action response, but the periodic tick task can emit an unchanged telemetry snapshot first, causing the assertion to fail nondeterministically.

**Triggers:** When the 10 Hz tick runs between `send` and the test's `rx.recv()`.

**Suggested fix:** Read telemetry until the message contains the expected asset binding, or stop/coordinate the tick task while asserting the action response.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +332 to +348
if action == "set_asset" {
let namespace = params
.get("namespace")
.and_then(Value::as_str)
.unwrap_or("");
let name = params.get("name").and_then(Value::as_str).unwrap_or("");
if namespace.trim().is_empty() || name.trim().is_empty() {
return Err("simulation: set_asset requires namespace and name".into());
}
if params
.get("version")
.and_then(Value::as_str)
.is_some_and(|version| version.trim().is_empty())
{
return Err("simulation: set_asset version must not be empty".into());
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (bug_risk): set_asset 接受任意非空的命名空间、名称和可选版本,并在未查询或验证 Hub 模型的情况下将其作为 simulation_asset 发出,因此任意或不存在的身份都会被客户端当作已验证的 Hub 绑定。

触发条件: 客户端发送了语法有效但不存在、未发布或未经验证的 Hub 身份时。

建议修复: 在修改状态之前,通过 Hub 验证/目录边界解析该引用;或者将此操作重命名并记录为接受未经验证的身份。

Original comment in English

issue (bug_risk): set_asset accepts any non-empty namespace, name, and optional version and emits it as simulation_asset without consulting or verifying a Hub model, so arbitrary or nonexistent identities are presented to clients as verified Hub bindings.

Triggers: When a client sends a syntactically valid but nonexistent, unpublished, or unverified Hub identity.

Suggested fix: Resolve the reference through the Hub verification/catalog boundary before mutating state, or rename/document the action as accepting an unverified identity.

Comment on lines +442 to +482
let update = rx.recv().await.expect("action telemetry");
let payload: Value = serde_json::from_slice(&update.data).expect("json");
assert_eq!(payload["properties"]["target_temperature"], json!(80.0));
transport.stop().await.expect("stop");
}

#[tokio::test]
async fn binds_and_clears_a_hub_asset_at_runtime() {
let config = SimulationConfig::default();
let device = config.devices.first().expect("default heater");
let (tx, mut rx) = mpsc::unbounded_channel();
let transport =
SimulationTransport::new(config.world_id, config.engine, device, config.tick_hz, tx)
.expect("kinematic backend");
transport.start().await.expect("start");
let _ = rx.recv().await.expect("initial telemetry");

let command = DeviceCommand {
command_id: "asset-command".into(),
device_id: device_id_for("lab-sim", &device.id),
action: "set_asset".into(),
params: json!({
"namespace": "scienceol",
"name": "heater-dalong",
"version": "1.0.0"
}),
};
transport
.send(&serde_json::to_vec(&command).expect("encode"))
.await
.expect("bind asset");
let bound = rx.recv().await.expect("asset telemetry");
let payload: Value = serde_json::from_slice(&bound.data).expect("json");
assert_eq!(
payload["properties"]["simulation_asset"],
json!({
"namespace": "scienceol",
"name": "heater-dalong",
"version": "1.0.0"
})
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (testing): 测试在发送 set_asset 后读取下一条遥测消息,并假定它是操作响应;但周期性 tick 任务可能会先发出未更改的遥测快照,导致断言出现非确定性失败。

触发条件: 10 Hz 的 tick 在 send 和测试的 rx.recv() 之间运行时。

建议修复: 持续读取遥测消息,直到消息包含预期的资产绑定;或者在断言操作响应时停止 tick 任务或与其协调。

Original comment in English

issue (testing): The test reads the next telemetry message after sending set_asset and assumes it is the action response, but the periodic tick task can emit an unchanged telemetry snapshot first, causing the assertion to fail nondeterministically.

Triggers: When the 10 Hz tick runs between send and the test's rx.recv().

Suggested fix: Read telemetry until the message contains the expected asset binding, or stop/coordinate the tick task while asserting the action response.

@Mile-Away
Mile-Away force-pushed the codex/simulation-hub-model-binding branch from 534396f to dffda52 Compare September 26, 2026 16:21
@Mile-Away
Mile-Away merged commit 33b716a into main Sep 26, 2026
14 checks passed
@Mile-Away
Mile-Away deleted the codex/simulation-hub-model-binding branch September 26, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant