Hrithik Shetty
All projects

UR-Robot Bartender

Grasshopper + SICK Lidar sensor

Fig. The robot locating and serving a cup from live sensor data
A Universal Robots UR-10e arm being guided by a human hand
Fig. The Universal Robots UR-10e in the TH OWL robotics lab

A robot arm that has been taught a pouring motion is not doing anything difficult. It repeats the same arc whether or not there is a cup underneath it. The interesting question is the one the seminar kept circling — what can the robot really do? — and the honest answer is: only as much as it can see.

So the cup stopped being a coordinate. A SICK lidar sensor watches the table, and everything downstream is derived from what it finds there: where the cup is, how wide it is, how much it holds, and therefore how many seconds to keep pouring. Move the cup, re-scan, and every one of those numbers changes. Nothing about it is hard-coded.

  1. SICK SOPASEngineering tool · capture and filter
  2. PythonCoLa transport protocol · writes .ply
  3. Rhino + GrasshopperRobots plugin · trajectory and control
Type
Group project
Team
Hrithik Shetty, Bo Schneider
My part
Sensor pipeline and Grasshopper logic, jointly with Bo
Seminar
Collaborative Robotics in Architecture
Supervision
Victor Sardenberg, Ph.D.
Location
Detmold, Germany
Course
TH OWL — MID Computational Design
Diagram: the SICK sensor scans a mug on a table and connects by cable to a laptop, which sends instructions wirelessly to the UR-10e arm
Fig. The whole loop. The sensor is wired to the laptop, the laptop talks to the robot — and the only thing travelling between them is a point cloud and the numbers pulled out of it.

The trial that came first

Before the sensor was involved at all, the same arm learned to open a bottle and pour it into a mug at a position we typed in. That version taught us the constraint that shaped everything after it: each joint on the UR-10e only travels ±360°, so a cap that needs more than one full turn will run the wrist into its own limit mid-unscrew.

The fix is unglamorous and worth stating. The robot turns the cap 180° anticlockwise, lets go, lifts clear, rotates back clockwise to where it started, and takes a fresh grip. Threads longer than half a turn stop being a special case.

Stack

  • SICK Visionary-S CX
  • SOPAS Engineering Tool
  • CoLa protocol
  • Point clouds (.ply)
  • Robots plugin
  • UR-10e

Teaching it to look

The sensor is a SICK Visionary-S CX on a stand above the table. Its own software, the SOPAS Engineering Tool, is where the scan gets useful: a dynamic distance filter, a Z-based filter, an isolated-pixel filter and a median filter, tuned until the table, the paper and the room fall away and the cup is most of what survives.

That tuning matters more than it sounds. Those filters are saved onto the sensor itself, so the capture script that runs later inherits them and returns a cloud that is already mostly cup.

Hurdles

  • ±360° joint limits
  • Plane orientation
  • Sensor-to-robot coordinates
  • Constant pour rate
SOPAS Engineering Tool showing the scene as a depth-coloured point cloud, with the sensor model above the table and filter settings on the right
Depth. The same table read as distance. The slab is the tabletop; the green lump standing proud of it is the cup.
SOPAS Engineering Tool showing the same scene as a colour point cloud beside the live camera view
Colour. The same frame with the colour channel on, next to the live view — useful for checking the filters cut the room and not the cup.

Getting the data out

The lidar streams over SICK’s native CoLa protocol, which Grasshopper cannot read. Building on the sick_visionary_samples package from SICK AG, the capture script opens a TCP control connection and a stream on port 2114, and sets the one number that shapes everything after it: ten seconds between captures.

Then it stays in a loop. Each pass triggers a single snapshot, reads the frame off the stream in millimetres, and turns its depth map into a point cloud — real-world coordinates rather than pixels.

The detail that makes the loop useful is the file name. Every capture overwrites the same world_coordinates.ply, so Grasshopper never has to be told which scan is newest — it re-reads one path and always gets the current table. Move the cup, wait one cycle, and the graph is already looking at it.

The wait is measured, not fixed. The time the capture itself took is subtracted from the interval, so a slow frame does not push every later one back, and the scans stay ten seconds apart.

Ctrl+C ends the loop and reports how many frames it took.

HS_bigRobotTimerSnapshot.py Python · excerpt

def runSnapshotsDemo(ip_address: str, device_type: str):
    cola_protocol, control_port, _ = get_device_config(device_type)
    transport_protocol = "TCP"
    streaming_port = 2114
    write_files = True
    capture_interval = 10.0 #The time between two picture/pointclouds taken
    # …
    try:
        frame_count = 0
        # …
        while True:
            start_time = time.time()
            frame_count += 1

            # Trigger a snapshot
            device_control.singleStep()
            if transport_protocol == "TCP":
                streaming_device.getFrame()
                # …
                whole_frame = streaming_device.frame
                sensor_data.read(whole_frame, convertToMM=True)
                # …
                if sensor_data.hasDepthMap:
                    # …
                    if write_files:
                        world_coordinates, dist_data = convertToPointCloud(sensor_data.depthmap.distance,
                                                                        sensor_data.depthmap.intensity,
                                                                        sensor_data.depthmap.confidence,
                                                                        sensor_data.cameraParams, sensor_data.xmlParser.stereo)
                        # …
                        #This overwrite the existing PLY
                        writePointCloudToPLY(os.path.join(
                            pcl_dir, "world_coordinates.ply"), world_coordinates)
                        # …

            # Calculate time to wait for next capture
            time_elapsed = time.time() - start_time
            time_to_wait = max(0, capture_interval - time_elapsed)
            print(f"Waiting {time_to_wait:.2f} seconds until next capture...")
            time.sleep(time_to_wait)

    except KeyboardInterrupt:
        print(f"\nCapture stopped after {frame_count} frames")
Fig. The capture loop — one snapshot every ten seconds, always written to the same file Built on sick_visionary_samples by SICK AG (Unlicense); the timed loop and output handling are ours.

It is a small script and deliberately so. It captures and it writes; every decision about what the cloud means is left to the graph, where it can be seen and changed.

The full Grasshopper definition, with labelled groups for reading the lidar data, bottle positions, calculating time to fill the cup, commands, tool, loading the robot and the final program
Fig. One definition, read left to right — the point cloud arrives at the far left and leaves as a robot program at the right

From a cloud to a number

The graph opens the .ply, throws away everything that is not a point, and finds the highest Z value in what remains. A tolerance band below that height is kept, which isolates the rim of the cup rather than its sides, and a circle is fitted through it. That circle gives the cup’s centre and its radius in one move — the centre becomes the plane the robot pours at, the radius feeds the volume.

Fitting a circle rather than hunting for a centroid is the part worth keeping. A centroid drifts with whatever noise survives the filters; three points on a rim do not.

The pour is arithmetic

The rim circle is extruded down to the table, capped, and Grasshopper reports a volume. Multiply by a fill factor of 0.6 — nobody wants a glass filled to the brim — divide by the measured flow of the spout, and the answer is a number of seconds.

On the run captured here that came out at 35.67 seconds, from a flow of roughly 0.0153 litres per second and a table height of 797 mm. Change the cup and every one of those figures moves.

Grasshopper group reading the point cloud: raw data, cleaning, finding the largest Z value, a tolerance band below it, a fitted circle and the resulting orientation plane
Fig. Raw file to target plane — clean the points, find the highest one, keep a band below it, fit a circle
Grasshopper groups calculating time to fill the cup and issuing robot commands, with panels reading seconds of pouring and litres per second
Fig. Volume becomes seconds, and seconds become a wait instruction the controller understands

Two bottles, one cup

With a target plane and a duration, the rest is choreography: pick the first bottle, carry it to the cup, rotate it to the angle where liquid actually leaves the spout, hold for the calculated time, rotate back, return it, and repeat with the second. The tilt is its own small problem — pour too shallow and nothing comes out, too steep and the rate stops being constant, which breaks the arithmetic the whole thing rests on.

Then it wipes the table. A sponge is picked up and run over the drips, because a bartender that leaves a mess has not finished the job.

Robot control

  • Gripper level
  • Custom commands
  • Wait / dwell
  • Plane orientation
  • Pick and place
Grasshopper groups for picking each bottle, saving a spot, rotating at the bottle and cup, pouring, and cleaning the table afterwards
Fig. The routine, group by group — pick, rotate, pour, return, and then clean up after itself
Close-up of the SICK Visionary-S sensor in its bracket, mounted above the workspace
Fig. The Visionary-S in its bracket, looking down at the table
Line drawing: the sensor on its stand scans a cup on a table, the robot arm remarks that it can calculate complex trajectories and this is what it is asked to do, while three people cheer
Fig. Six degrees of freedom, one drink. The seminar asked what a robot can really do — this is what we made it do.