ROS 2 (Humble) package for a Baumer OXP200 laser line-profile scanner mounted
on a robot arm. Ported from an earlier ROS 1/catkin package to run the same
filter → accumulate/denoise pipeline shape as the
pc_scanner RealSense package, so a downstream (e.g. Rhino)
process can consume either sensor's output the same way.
Unlike the RealSense (a full-frame depth camera), the OXP200 is a 1D line-profile sensor: each reading is a single scan line (X across, Z forward distance), and a full surface only comes from sweeping the sensor across the part with the robot. Practically, that means:
- The fixed-frame-count
capture_averagedservice works the same as inpc_scanner(useful for denoising a single static reading). - The real scanning workflow is a sweep: call
start_sweep_capture, move the robot while the scanner streams profiles (each one transformed intorobot_base_frameas it arrives, since the arm is moving throughout), then callstop_sweep_captureto voxel-fuse everything collected and publish it.
There's no range/region filter node in this package (unlike pc_scanner's
pointcloud_range_filter) — the OX200's own field-of-view/distance
configuration (set via its web interface) already constrains the raw data
well enough, so accumulate_denoise subscribes directly to the driver's
output.
scanner_pointcloud_node— connects to the OX scanner over the network (vendorOxApiC++ SDK, vendored underOxSDK_V2_0_0/) and publishes a rawPointCloud2per profile in thelaser_scannerframe, on/scanner/points.accumulate_denoise.py— same voxel + statistical-outlier fusion aspc_scanner, plus two new services for sweep capture (see below).
- Configure the scanner via its web interface — use Height Mode, not Distance Mode.
- Note its IP (default assumed here:
192.168.0.250).
ros2 launch baumer_ox_scanner baumer_scanner.launch.py host:=192.168.0.250This starts: the tool0 -> laser_scanner static transform, the scanner
driver, accumulate_denoise (with via_robot:=true by default — see below),
and rviz.
ros2 service call /start_sweep_capture std_srvs/srv/Trigger
# ...move the robot through the sweep...
ros2 service call /stop_sweep_capture std_srvs/srv/TriggerThe fused result is published on /scanner/points_accumulated in
robot_base_frame, and keeps being republished at republish_rate Hz so
late subscribers (or Rhino) still see it.
ros2 service call /reset_accumulation std_srvs/srv/Triggerclears a running scan (either mode) without restarting the node.
ros2 service call /capture_averaged std_srvs/srv/TriggerAverages num_frames consecutive profiles from the current, stationary
pose — a literal port of pc_scanner's snapshot mode, kept in case a static
reading is ever useful for this sensor.
Both capture modes need tf2 to resolve robot_base_frame -> ... -> robot_arm_tool0 -> laser_scanner. The tool0 -> laser_scanner static
transform in launch/baumer_tf.launch.py is currently a placeholder
(identity) — measure the actual sensor mount offset (CAD or hand-eye
calibration) before trusting fusion results, the same caveat as
pc_scanner's camera mount transform.
scanner_pointcloud_node:
| Parameter | Default | Meaning |
|---|---|---|
host |
192.168.0.250 |
Scanner IP address |
frame_id |
laser_scanner |
Frame the raw cloud is published in |
topic_name |
/scanner/points |
Raw output topic |
rate |
50.0 Hz |
Profile read/publish rate |
measurement_distance |
0.25 m |
Nominal standoff distance (z offset) |
unit_to_meters_override |
0.0 (auto-detect) |
Force the raw-unit-to-meters factor instead of reading it from the sensor (see below) |
Each profile point comes back as (X[i]+XStart)/Z[i] in raw sensor
ticks. Per the vendor SDK docs, the actual value is
raw / profile.Precision, in whatever unit GetProfileInfo() reports (their
own worked example: Precision=10, unit "mm") — Precision is a live,
device/configuration-dependent value, not a fixed constant. On each
(re)connect, scanner_pointcloud_node calls GetProfileInfo() once, maps the
reported unit string (m/mm/cm/nm/um/µm) to a meters factor, and
logs what it detected. If the unit string doesn't match any of those (or you
want to force a specific value), set unit_to_meters_override explicitly —
otherwise it falls back to mm (0.001) with a loud warning.
accumulate_denoise shares the same parameters as pc_scanner's (see its
README), just with input_topic defaulting to /scanner/points and
output_topic to /scanner/points_accumulated.
| Service | Type | Effect |
|---|---|---|
/capture_averaged |
std_srvs/srv/Trigger |
Average num_frames frames from a static pose |
/start_sweep_capture |
std_srvs/srv/Trigger |
Begin fusing profiles into robot_base_frame as the robot moves |
/stop_sweep_capture |
std_srvs/srv/Trigger |
End the sweep, voxel-fuse, and publish the result |
/reset_accumulation |
std_srvs/srv/Trigger |
Clear a running scan (either mode) |
This could be due to not closing the scanner's WebInterface, or another instance of the driver node already running and connected to the scanner.
scanner_pointcloud_node publishes the raw topic with SensorDataQoS (best
effort), matching the RealSense driver convention pc_scanner already relies
on. If you add an rviz PointCloud2 display for /scanner/points directly
(e.g. to debug the raw data), rviz's default per-display Reliability is
Reliable, which is incompatible with a best-effort publisher — rviz will
warn and nothing will render. Set that display's Topic Reliability
Policy to Best Effort to fix it. /scanner/points_accumulated (the
final fused output from accumulate_denoise) doesn't have this problem — it
publishes Reliable by default, matching baumer_scan.rviz's existing display.