How to Connect External Sensors to FreeCAD Using Python

Connecting external hardware and sensors to FreeCAD transforms the parametric 3D modeling software into an interactive digital twin or real-time visualization platform. By leveraging FreeCAD’s embedded Python interpreter along with serial communication libraries, you can ingest live telemetry from microcontrollers like Arduino, Raspberry Pi, or ESP32 and directly manipulate 3D geometry properties. This guide outlines the essential steps to configure communication, handle asynchronous data streams, and update FreeCAD document objects in real time.

1. Install Required Python Libraries

FreeCAD uses its own internal Python environment. To communicate with external hardware over USB or serial ports, you must install the pyserial library into FreeCAD's Python environment.

Open the FreeCAD Python console (View > Panels > Python console) and run:

import subprocess
import sys

subprocess.run([sys.executable, "-m", "pip", "install", "pyserial"])

2. Configure Hardware Data Output

Set up your microcontroller (e.g., Arduino) to read your sensor (such as an ultrasonic distance sensor, potentiometer, or IMU) and print structured values over the serial interface.

Example Arduino snippet:

void setup() {
  Serial.begin(9600);
}

void loop() {
  int sensorValue = analogRead(A0); // Read sensor
  Serial.println(sensorValue);      // Send raw value followed by newline
  delay(50);                        // Limit transmission rate
}

3. Implement Non-Blocking Data Polling with PySide

Reading serial data inside a continuous while loop within FreeCAD will block the main application thread and freeze the user interface. To prevent this, use PySide's QTimer to poll the serial port periodically without interrupting FreeCAD’s event loop.

Run the following script inside FreeCAD's Macro editor (Macro > Macros > Create):

import FreeCAD as App
import FreeCADGui as Gui
from PySide import QtCore
import serial

# Configure serial connection
SERIAL_PORT = "COM3"  # Replace with your port (e.g., '/dev/ttyUSB0' on Linux)
BAUD_RATE = 9600

try:
    ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=0.1)
except Exception as e:
    App.Console.PrintError(f"Failed to connect to serial port: {e}\n")
    ser = None


def read_sensor_and_update():
    if ser and ser.in_waiting > 0:
        try:
            raw_line = ser.readline().decode("utf-8").strip()
            if raw_line:
                sensor_value = float(raw_line)

                # Reference your target 3D object in the active document
                doc = App.ActiveDocument
                obj = doc.getObject("Box")  # Name of your target object

                if obj:
                    # Update a property based on sensor data
                    # Example: Mapping sensor value to box height
                    obj.Height = sensor_value

                    # Recompute the document to reflect visual changes
                    doc.recompute()
        except ValueError:
            pass  # Handle incomplete serial strings safely


# Setup QTimer for polling every 50 milliseconds
if ser and ser.is_open:
    timer = QtCore.QTimer()
    timer.timeout.connect(read_sensor_and_update)
    timer.start(50)

    # Keep a reference to prevent garbage collection
    App.sensor_timer = timer
    App.Console.PrintMessage("Sensor listening active.\n")

4. Drive Parametric Properties

Once the data stream is linked via QTimer, you can alter any standard FreeCAD property:

  • Linear Position: Modify obj.Placement.Base.x, y, or z to simulate actuators or positioning stages.
  • Rotational Orientation: Apply sensor readings from gyroscopes to obj.Placement.Rotation using Euler angles or quaternions.
  • Dimensions: Update parametric parameters such as Length, Width, Radius, or custom Sketcher constraints to show dynamic deformation or fluid levels.

Always call doc.recompute() after modifying values to ensure the 3D viewport updates immediately.

5. Stopping the Data Stream

To safely close the connection and stop polling without restarting FreeCAD, execute:

if hasattr(App, "sensor_timer"):
    App.sensor_timer.stop()
    del App.sensor_timer

if "ser" in locals() and ser.is_open:
    ser.close()

App.Console.PrintMessage("Sensor communication stopped.\n")