Communication with Blender via sockets

I’m always curious about expanding functionality and improving workflow by connecting devices to software that I already enjoy using. I love using Blender and making custom controllers for creating computer graphics is something very interesting to me.

As long as a program has a scripting environment or a plugin support, you can usually connect anything you like to it. In one of my previous experiments I’ve added a serial communication between Blender and a custom device. It was a bit clunky, obviously. Serial communication isn’t something you want to be run as a Blender’s subprocess.

A slightly more civilized solution would be to spawn a server instance on Blender’s side and poll for incoming messages. I’ve tried to implement something like that with sockets module. The idea was to see if a solution like that would mess with the primary process and how well does it handle communication with an outside world.

You’ll find the code below:

import socket
import sys
import bpy

HOST = '127.0.0.1'
PORT = 65432

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))

# listen() marks the socket referred to by sockfd as a passive socket (awaits for an in­coming connection,
# which will spawn a new active socket once a connection is established), that is, as a socket that
# will be used to accept incoming connection requests using accept(2).
s.listen()

# Extracts the first connection request on the queue of pending connections for the listening socket,
# sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket.
# The newly created socket is not in the listening state. The original socket sockfd is unaffected by
# this call.
conn, addr = s.accept()
conn.settimeout(0.0)


def handle_data():
    interval = 0.1
    #print('Connected by: ', addr)
    data = None

    # In non-blocking mode blocking operations error out with OS specific errors.
    # https://docs.python.org/3/library/socket.html#notes-on-socket-timeouts
    try:
        data = conn.recv(1024)
    except:
        pass

    if not data:
        pass
    else:
        conn.sendall(data)
       
        # Fetch the 'Sockets' collection or create one. Anything created via sockets will be linked
        # to that collection.
        collection = None
        try:
            collection = bpy.data.collections["Sockets"]
        except:
            collection = bpy.data.collections.new("Sockets")
            bpy.context.scene.collection.children.link(collection)

        if "cube" in data.decode("utf-8"):
            mesh_data = bpy.data.meshes.new(name='m_cube')
            obj = bpy.data.objects.new('cube', mesh_data)
            collection.objects.link(obj)

        if "empty" in data.decode("utf-8"):
            empty = bpy.data.objects.new("empty", None)
            empty.empty_display_size = 2
            empty.empty_display_type = 'PLAIN_AXES'
            collection.objects.link(empty)


        if "quit" in data.decode("utf-8"):
            conn.close()
            s.close()

    return interval

bpy.app.timers.register(handle_data)

The script starts by importing necessary modules and creating a TCP socket, binded to the localhost on port 65432.

Then a bit of sockets magic happens (it’s not that magical or complex…). A socket is instantiated. That socket waits for a connection, and as soon as a client connects it creates another socket, called conn (for connection).

The next interesting part (skip the handle_data definition for a second) is a call to Blender’s module bpy.app.timers.register(handle_data). This function registers a timer which will fire the handle_data function. That function needs to return time, in seconds, after which the timer should execute it again.

The handle_data checks if there is any data to be processed. If it finds keywords: empty or cube in the message, it creates a new Empty object or a Cube mesh. Those will be put under Sockets collection.

It also recognizes a quit message. I’ve implemented that because after you run the script in Blender’s environment you loose control. I’ve been iterating on the script and not closing the socket correctly meant that it was still there, after the script exited, binded to the port. That meant that I had to restart Blender.

This script is very rough. You have to start it manually every time you want to connect again. The s.accept() is a blocking operation which means Blender hangs, until a client connects. As I’ve mentioned it was a test, an experiment. It does work and doesn’t impact the Blender’s main loop.

You can test that script by running it from the Blender’s scripting tab, starting netcat in a terminal like so:

nc localhost 65432

That opens a prompt in which you can type the commands. You type cube, smash that Enter key and Blender spawns a cube.

I wish the idea of interprocess communication was a part of the core design of applications like Blender. It’s very common to have a scripting environment in apps like that. They always feel a bit hacky and tacked on.