Skip to content

Conversation

@habimiao
Copy link

@habimiao habimiao commented Oct 21, 2025

四川宜宾线下高分子组组态图完整搭建结果

Summary by Sourcery

Add support for IKA heater‐stirrer devices via NAMUR protocol and update device and resource registries to handle new mappings and identifiers

New Features:

  • Introduce IkaNamurClient and HeaterStirrer_IKA classes for RS-232 NAMUR protocol support
  • Register heaterstirrer.ika device in the temperature devices registry with stir and temperature control commands
  • Add fluid_port_5 mapping to the syringe pump with valve device
  • Allow injecting device_uuid in the serial_node preset initialization
  • Extend resource_tracker to map "container" and "resource" to the Container PLR class

Tests:

  • Add Simple_polyer.json experiment configuration file

@sourcery-ai
Copy link

sourcery-ai bot commented Oct 21, 2025

Reviewer's Guide

Complete the Yibin offline high-polymer configuration by introducing IKA heater-stirrer support via a NAMUR serial client, updating registry entries for temperature and valve devices, extending resource type mappings, and wiring device_uuid propagation in ROS nodes.

Sequence diagram for device_uuid propagation in serial_node initialization

sequenceDiagram
  participant "SerialNode"
  participant "BaseROS2DeviceNode"
  "SerialNode"->>"BaseROS2DeviceNode": __init__(device_uuid, driver_instance, device_id, status_types)
  "SerialNode"->>"BaseROS2DeviceNode": Passes device_uuid from kwargs or generates new UUID
Loading

ER diagram for updated temperature device registry

erDiagram
  heaterstirrer_ika {
    port string
    baudrate integer
    status string
    stir_speed number
    temp number
    temp_target number
  }

  heaterstirrer_ika ||--o{ action_value_mappings : has
  heaterstirrer_ika ||--o{ status_types : has

  action_value_mappings {
    auto-set_stir_speed
    set_temp_target
  }

  status_types {
    status str
    stir_speed float
    temp float
    temp_target float
  }
Loading

ER diagram for updated valve device registry (SY03B-T06)

erDiagram
  syringe_pump_with_valve_runze_SY03B_T06 {
    fluid_port_1
    fluid_port_2
    fluid_port_3
    fluid_port_4
    fluid_port_5
    fluid_port_6
  }

  syringe_pump_with_valve_runze_SY03B_T06 ||--o{ fluid_port_5 : has
Loading

Class diagram for new IKA heater-stirrer integration

classDiagram
  class IkaNamurClient {
    +port: str
    +baud: int
    +timeout: float
    +_ser: serial.Serial | None
    +open()
    +close()
    +send(*tokens: str): str
    +read_name(): str
    +read_speed(): str
    +read_speed_setpoint(): str
    +set_speed(rpm: int): str
    +start(): str
    +stop(): str
  }

  class HeaterStirrer_IKA {
    +_status: str
    +_stir_speed: float
    +_temp_target: float
    +_cli: IkaNamurClient
    +status: str
    +stir_speed: float
    +set_stir_speed(speed: float)
    +temp_target: float
    +set_temp_target(temp: float)
    +temp: float
    +start_stir(vessel, stir_speed: float, purpose: str): bool
    +stop_stir(vessel): bool
    +stir(stir_time: float, stir_speed: float, settling_time: float, **kwargs): bool
    +close()
  }

  IkaNamurClient <.. HeaterStirrer_IKA : uses
Loading

Class diagram for updated resource type mapping in resource_tracker.py

classDiagram
  class ResourceDictInstance {
    +res_content: ResourceContent
  }

  class ResourceContent {
    +name: str
    +uuid: str
    +type: str
  }

  class PLRResource {
    +type: str
  }

  ResourceDictInstance o-- ResourceContent

  class ResourceTypeMap {
    +TYPE_MAP: dict
  }

  ResourceTypeMap : plate -> Plate
  ResourceTypeMap : well -> Well
  ResourceTypeMap : deck -> Deck
  ResourceTypeMap : container -> Container
  ResourceTypeMap : resource -> Container
Loading

File-Level Changes

Change Details Files
Add IKA heater-stirrer device definition to temperature registry
  • Define action mappings for set_stir_speed and set_temp_target commands
  • Configure module path, status_types, init_param_schema, and version
  • Include descriptive metadata and empty config_info/handles sections
unilabos/registry/devices/temperature.yaml
Expand resource type mapping in resource_tracker
  • Extend TYPE_MAP to map “container” and “resource” to Container
unilabos/ros/nodes/resource_tracker.py
Add fluid_port_5 entry for six-way valve
  • Introduce data_key fluid_port_5 with io_type,target, handler_key and metadata
unilabos/registry/devices/pump_and_valve.yaml
Expose device_uuid in BaseROS2DeviceNode initialization
  • Pass device_uuid from kwargs (defaulting to a new UUID) into serial node constructor
unilabos/ros/nodes/presets/serial_node.py
Implement IKA NAMUR serial client and HeaterStirrer_IKA driver
  • Create IkaNamurClient with open/close/send and common commands
  • Add contextmanager ika_client
  • Implement HeaterStirrer_IKA with properties, synchronous serial calls offloaded to asyncio threads, and protocol-compliant stir/stop/stir-settle methods
unilabos/devices/heaterstirrer/ika.py
Add sample polymer experiment JSON
  • Create Simple_polyer.json as a placeholder for test experiment config
test/experiments/Simple_polyer.json

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

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • In IkaNamurClient.send(), the loop condition while time.time() < end or self._ser.in_waiting could spin indefinitely if the port always shows pending bytes; consider switching to an AND condition or adding a secondary timeout to guarantee exit.
  • The HeaterStirrer_IKA.temp property returns the setpoint rather than an actual reading; either implement a real NAMUR query for current temperature or clearly document that it only reflects the target.
  • Defaulting to a random UUID in serial_node.py on each init may break stable device identity across restarts; consider requiring a consistent device_uuid injection or persisting it externally.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In IkaNamurClient.send(), the loop condition `while time.time() < end or self._ser.in_waiting` could spin indefinitely if the port always shows pending bytes; consider switching to an AND condition or adding a secondary timeout to guarantee exit.
- The HeaterStirrer_IKA.temp property returns the setpoint rather than an actual reading; either implement a real NAMUR query for current temperature or clearly document that it only reflects the target.
- Defaulting to a random UUID in serial_node.py on each init may break stable device identity across restarts; consider requiring a consistent device_uuid injection or persisting it externally.

## Individual Comments

### Comment 1
<location> `unilabos/devices/heaterstirrer/ika.py:146-148` </location>
<code_context>
+    except (ValueError, TypeError):
+      speed_int = 0
+    # 同步串口调用放入线程,避免阻塞事件循环
+    await asyncio.to_thread(self.set_stir_speed, speed_int)
+    return True
+
+  async def stop_stir(self, vessel) -> bool:
</code_context>

<issue_to_address>
**suggestion:** Using asyncio.to_thread for synchronous serial calls is appropriate, but error handling is minimal.

Add exception handling for errors during serial communication to improve robustness.

```suggestion
    # 同步串口调用放入线程,避免阻塞事件循环
    try:
        await asyncio.to_thread(self.set_stir_speed, speed_int)
        return True
    except Exception as e:
        import logging
        logging.error(f"Error during serial communication in set_stir_speed: {e}")
        return False
```
</issue_to_address>

### Comment 2
<location> `unilabos/devices/heaterstirrer/ika.py:49-50` </location>
<code_context>
  def send(self, *tokens: str) -> str:
    if not self._ser or not self._ser.is_open:
      self.open()
    line = " ".join(tokens).upper()
    payload = (line + "\r\n").encode("ascii")
    self._ser.reset_input_buffer()
    self._ser.write(payload)
    time.sleep(0.25)
    buf = bytearray()
    end = time.time() + self.timeout
    while time.time() < end or self._ser.in_waiting:
      data = self._ser.read(self._ser.in_waiting or 1)
      if data:
        buf.extend(data)
      else:
        time.sleep(0.02)
    return buf.decode("ascii", errors="ignore").strip()

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
      if data := self._ser.read(self._ser.in_waiting or 1):
```
</issue_to_address>

### Comment 3
<location> `unilabos/devices/heaterstirrer/ika.py:106` </location>
<code_context>
  def set_stir_speed(self, speed: float):
    speed_int = int(float(speed))
    self._cli.set_speed(speed_int)
    if speed_int > 0:
      self._cli.start()
    else:
      self._cli.stop()
    self._stir_speed = float(speed_int)

</code_context>

<issue_to_address>
**suggestion (code-quality):** Remove unnecessary casts to int, str, float or bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))

```suggestion
    speed_int = int(speed)
```
</issue_to_address>

### Comment 4
<location> `unilabos/devices/heaterstirrer/ika.py:119` </location>
<code_context>
  def set_temp_target(self, temp: float):
    self._temp_target = float(temp)
    self._cli.send("OUT_SP_1", f"{int(self._temp_target)}")
    self._cli.send("START_1")

</code_context>

<issue_to_address>
**suggestion (code-quality):** Remove unnecessary casts to int, str, float or bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))

```suggestion
    self._temp_target = temp
```
</issue_to_address>

### Comment 5
<location> `unilabos/devices/heaterstirrer/ika.py:130-132` </location>
<code_context>
  def _extract_vessel_id(self, vessel) -> str:
    if isinstance(vessel, dict):
      return str(vessel.get("id", ""))
    return str(vessel)

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))

```suggestion
    return str(vessel.get("id", "")) if isinstance(vessel, dict) else str(vessel)
```
</issue_to_address>

### Comment 6
<location> `unilabos/devices/heaterstirrer/ika.py:143` </location>
<code_context>
  async def start_stir(self, vessel, stir_speed: float, purpose: str = "") -> bool:
    """开始持续搅拌(协议动作)

    - vessel: 可为字符串或形如 {"id": "..."} 的字典
    - stir_speed: 目标转速 RPM
    - purpose: 可选用途描述(仅用于上层日志)
    """
    _ = self._extract_vessel_id(vessel)  # 当前实现不强依赖容器,仅做形参兼容
    try:
      speed_int = int(float(stir_speed))
    except (ValueError, TypeError):
      speed_int = 0
    # 同步串口调用放入线程,避免阻塞事件循环
    await asyncio.to_thread(self.set_stir_speed, speed_int)
    return True

</code_context>

<issue_to_address>
**suggestion (code-quality):** Remove unnecessary casts to int, str, float or bool ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))

```suggestion
      speed_int = int(stir_speed)
```
</issue_to_address>

### Comment 7
<location> `unilabos/devices/heaterstirrer/ika.py:165-173` </location>
<code_context>
  async def stir(self, stir_time: float, stir_speed: float, settling_time: float, **kwargs) -> bool:
    """定时搅拌 + 沉降(协议动作)

    - stir_time: 搅拌时间(秒)
    - stir_speed: 搅拌速度(RPM)
    - settling_time: 沉降时间(秒)
    其余 kwargs(如 vessel/time/time_spec/event)按协议形态传入,此处可忽略。
    """
    try:
      total_stir_seconds = max(0.0, float(stir_time))
    except (ValueError, TypeError):
      total_stir_seconds = 0.0
    try:
      speed_int = int(float(stir_speed))
    except (ValueError, TypeError):
      speed_int = 0
    try:
      total_settle_seconds = max(0.0, float(settling_time))
    except (ValueError, TypeError):
      total_settle_seconds = 0.0

    # 开始搅拌
    await asyncio.to_thread(self.set_stir_speed, speed_int)
    if total_stir_seconds > 0:
      await asyncio.sleep(total_stir_seconds)

    # 停止搅拌进入沉降
    await asyncio.to_thread(self.set_stir_speed, 0)
    if total_settle_seconds > 0:
      await asyncio.sleep(total_settle_seconds)

    return True

</code_context>

<issue_to_address>
**issue (code-quality):** Remove unnecessary casts to int, str, float or bool [×3] ([`remove-unnecessary-cast`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-cast/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +146 to +148
# 同步串口调用放入线程,避免阻塞事件循环
await asyncio.to_thread(self.set_stir_speed, speed_int)
return True
Copy link

Choose a reason for hiding this comment

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

suggestion: Using asyncio.to_thread for synchronous serial calls is appropriate, but error handling is minimal.

Add exception handling for errors during serial communication to improve robustness.

Suggested change
# 同步串口调用放入线程,避免阻塞事件循环
await asyncio.to_thread(self.set_stir_speed, speed_int)
return True
# 同步串口调用放入线程,避免阻塞事件循环
try:
await asyncio.to_thread(self.set_stir_speed, speed_int)
return True
except Exception as e:
import logging
logging.error(f"Error during serial communication in set_stir_speed: {e}")
return False

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