Skip to main content

Parsec

The Parsec node reads the multi-zone depth sensor and publishes distance data across a configurable 4×4 or 8×8 zone grid. Each zone returns a distance from 2 cm to 4 m. Data publishes in three formats simultaneously — a raw zone array, a grid image, and a 3D point cloud — select only the outputs your application needs.

Zone value encoding (mm):

  • 0 – 4000 — valid distance
  • -1 — zone disabled
  • -2 — invalid / no return

Configuration​

parsec:
enabled: true
sensors: ["parsec"]
parsec:
transport: can # "can" or "usb"
can_id: 5 # CAN transport only
serial_port: /dev/ttyACM0 # USB transport only
frame_id: "parsec_link"
publish_rate_hz: 15
publish_outputs: ["zones", "grid", "pointcloud"]

Parameters​

ParameterDescription
transportInterface: can or usb
can_idCAN bus device ID (CAN transport only)
serial_portUSB serial port path (USB transport only, default /dev/ttyACM0)
frame_idTF frame ID included in the message header
publish_rate_hzOutput rate in Hz — max 60 Hz at 4×4, max 15 Hz at 8×8
publish_outputsTopics to publish: any combination of zones, grid, pointcloud

Topics​

Published​

TopicTypeDescriptionRate
/<name>/zonesstudica_control/ParsecZoneMsgFlat distance array in mm, one value per zonepublish_rate_hz
/<name>/gridsensor_msgs/Image (16UC1)Zone grid as an image — each pixel holds a uint16 distance in mmpublish_rate_hz
/<name>/pointcloudsensor_msgs/PointCloud23D points in metres, one per valid zone, x/y estimated from grid positionpublish_rate_hz
/<name>/min_rangesensor_msgs/RangeNearest valid zone distance in metres — always publishedpublish_rate_hz
note

All four topics carry the same underlying zone grid in different ROS message formats. Enable only the outputs your application consumes.

Decoding the Grid Image​

Each pixel in the 16UC1 image holds a uint16 distance in millimetres. Use cv_bridge to convert:

from cv_bridge import CvBridge
import numpy as np

bridge = CvBridge()
img_mm = bridge.imgmsg_to_cv2(msg, desired_encoding='16UC1') # numpy uint16, shape (H, W)

To decode manually without cv_bridge:

# pixel at index i (0 .. width*height - 1)
mm = msg.data[2 * i] + (msg.data[2 * i + 1] << 8)

Service​

Service name: /<name>/parsec_cmd
Type: studica_control/SetData

CommandDescription
get_configReturns device settings as a string
get_zone_distanceDistance of a specific zone in mm — pass zone index in initparams.n_encoder
get_min_distanceDistance of the nearest valid zone in mm

Example​

#!/usr/bin/env python3
"""Parsec — subscribe to multi-zone ToF readings (100 frames, then exit).

Run: ros2 run studica_control parsec_example.py
Requires: studica_launch.py running, parsec enabled in params.yaml
sensors: ["parsec"] (name must match SENSOR below)
"""
import math

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Range
from studica_control.msg import ParsecZoneMsg
from studica_control.srv import SetData

SENSOR = 'parsec'
MAX_PRINTS = 100


def _zone_cell(d: int) -> str:
if d == -1:
return ' off'
if d == -2 or d < 1 or d > 4000:
return ' ---'
return f'{d:4d} mm'


class ParsecExample(Node):
def __init__(self):
super().__init__('parsec_example')
self.create_subscription(ParsecZoneMsg, f'/{SENSOR}/zones', self.on_zones, 10)
self.create_subscription(Range, f'/{SENSOR}/min_range', self.on_min_range, 10)
self.client = self.create_client(SetData, f'/{SENSOR}/parsec_cmd')
self.create_timer(5.0, self.poll_service)
self.print_count = 0

def on_zones(self, msg: ParsecZoneMsg):
side = 8 if msg.zones == 64 else 4
center = msg.zones // 2
center_mm = msg.fdist[center] if center < len(msg.fdist) else -2
self.get_logger().info(
f'{self.print_count:3d} | seq={msg.seq} count={msg.zones} center={center_mm} mm'
)
for row in range(side):
row_vals = msg.fdist[row * side:(row + 1) * side]
if not row_vals:
break
self.get_logger().info(' ' + ' '.join(_zone_cell(d) for d in row_vals))

self.print_count += 1
if self.print_count >= MAX_PRINTS:
rclpy.shutdown()

def on_min_range(self, msg: Range):
if math.isinf(msg.range):
self.get_logger().warn('no valid zones in frame')
else:
self.get_logger().info(f'nearest valid zone: {msg.range:.3f} m')

def poll_service(self):
if not self.client.wait_for_service(timeout_sec=1.0):
return
req = SetData.Request()
req.params = 'get_min_distance'
future = self.client.call_async(req)
future.add_done_callback(lambda f: self.get_logger().info(
f'get_min_distance: {f.result().message} mm'
))


def main():
rclpy.init()
rclpy.spin(ParsecExample())
rclpy.shutdown()