Light Painting with Dorna and Inkscape


In this tutorial, we will use Inkscape to draw an image and generate gcode that Dorna will use to draw a light painting. Inkscape is a vector graphics software that runs on Mac, Windows, and Linux. It has built-in extentions that will allow us to generate gcode that we can pass into the Dorna API. Here, we’ll draw star with an LED.

homed

Drawing an image and generating gcode


Before we get started drawing with Dorna, download and install Inkscape. We will use Inkscape to draw our images that Dorna will trace with a light. Once you’ve installed and opened Inkscape, you’ll see a window like this:

homed

Before we start drawing, let’s change some of Inkscape’s settings to make it easier for ourselves later on. First go to File > Document Properties and change the Display units to inches.

Under the Border section, uncheck the Show page border box.

homed
homed

It’s also helpful to have a grid on your Inkscape document. So before closing the Document Properties window, click on the Grids tab and select a new rectangular grid.

homed

You can now close the Document Properties window.

We’re going to use Inkscape’s built-in gcode tools to orient our points and generate gcode.

This is a good point to review Dorna’s coordinate system. We can define the position of Dorna using either the angles of the five joints, or the XYZ loaction of the toolhead relative to the origin. For this tutorial, we’ll work in cartesian coordinates. The origin (X=0, Y=0, Z=0) is the center of the bottom plane of the robot (base), where robot touches the ground. When the robot is outstretched as in the figure below, the loaction is (X=19.499, Y=0, Z=8.111). You can check the current location of Dorna with the .position() method in the Dorna API.

>>> robot.position() # values of the joints [j0,j1,j2,j3,j4]
'[0,0,0,0,0]'

>>> robot.position("xyz") # Cartesian position [x,y,z,a,b]
'[19.499,0,8.111,0,0]'

homed

When you draw in Inkscape, you are drawing on the XY plane, but we want Dorna to draw our image in front of itself, on the YZ plane. We’ll address this in our Python script later on, but for now, keep in mind that there will be a coordinate transformation.

Y_inkscape to Z_Dorna
X_inkscape to Y_Dorna
Z_inkscape to X_Dorna

Navigate to Extensions > Gcodetools > Orientation Points. Then set your Units to inches and set the Z Surface to 15.0 and the Z depth to 14.5. Then click Apply.

Since the gcode tools in Inkscape are intended for CNC machines, the Z Surface and Z depth parameters must be different. If you set Z Surface and Z depth to the same value, no gcode will be produced.

homed
homed

After you click Apply, you’ll notice that the orientation point at (5.0, 0.0, 14.5) is not lined up with your grid markers. Select it and move it to the right grid marker. This makes our drawing easier to follow later on.

homed

Your image will be drawn 14.5 inches in front of Dorna’s origin. This distance gives Dorna some room to move. You can experiment with this distance. Remember, Inkscape is using the XY plane and Dorna will use the YZ plane to draw. Now, let’s draw our image.

Make sure that Layer 1 is selected, then click on the Create stars and polygons button on the left sidebar. Draw a star with it’s center at X: 0.0, Y: 8.0. Then click the Select and transform objects button on the left sidebar.

homed

Now we need to convert this star into a path that can be translated into gcode that Dorna can follow. In the menu bar, select Path > Object to Path. You should see the following on the bottom of your document.

Path 10 nodes in Layer 1. Click selection to toggle scale/rotation handles.

Now we can convert our new path to gcode using Inkscape’s built in gcode tools. In the menu bar, navigate to Extensions > Gcodetools > Path to Gcode. Make sure your settings are as follows. Under the Preferences tab, name your file star.ngc select a directory where your gcode file will be saved.

homed

Click Apply. Your star should now be outlined with arrows, indicating the path, and you should have star_0001.ngc in the directory you chose.

homed

Draw path with Dorna


We’re now going to use the Dorna API to read star_0001.ngc and trace out the image in front of the robot. Before we start, physically place Dorna in the home position as in the image below. After the homing process, the angles of joints are:

>>> robot.position() # values of the joints [j0,j1,j2,j3,j4]
'[0, 145, -90, 0.0, 0.0]'

homed

Now create a new python script called LED_Image_Draw.py. Let’s start by loading the API and some useful modules.

from dorna import Dorna
import time
import json

Connect to the robot and save the output of robot.connect() in a variable. If the robot is connected, we’ll continue our script. If not, we’ll end the program. If the robot does not connect, try unplugging your USB connector and plug it in again.

robot = Dorna()
a = robot.connect()
parsed_json = json.loads(a)

if parsed_json['connection'] == 2:

    ## Draw our image ##

else:
    print("Error: The robot is not connected.")

Once we’ve connected succesfully, let’s home the robot. The term homing in this context means setting the absolute robot coordinates to a known coordinate. Once we’ve homed the robot, let’s move the robot to the outstretched position (X=19.499, Y=0, Z=8.111).

# REMEMBER TO PLACE ROBOT IN HOME POSITION PHYSICALLY #
robot.home("j0")
robot.home("j1")
robot.home("j2")
robot.home("j3")

start  = {"command": "move", "prm": {"path": "joint", "movement": 0, "speed": 1500.0, "j0":0,"j1": 0,"j2": 0,"j3":0,"j4":0}}
robot.play(start)

Now we need to read our gcode file and extract the gcode commands we will pass to Dorna. Remember that Inkscape’s gcode tools are intended for CNC machines which operate on the XY plane, where the Z axis is up and down. We want Dorna to draw our image in front of itself, on the YZ plane. To accomlish this, we need to make a simple coordinate transformation.

Y_inkscape to Z_Dorna
X_inkscape to Y_Dorna
Z_inkscape to X_Dorna

Then we’ll store the modified gcode in a list gc.

with open('star_0001.ngc') as f:
    content = f.readlines()

gc = []

for i in range(0,len(content)):
    if (content[i][1:7] == 'Footer'):
        break
    if (content[i][0:3] == 'G00' or content[i][0:3] == 'G01'):
            content[i] = content[i].replace('X','t1').replace('Y','t2').replace('Z','X').replace('t2','Z').replace('t1','Y')
            content[i] = content[i][:content[i].find(' F')]
            gc.append("G90")
            gc.append(content[i])

This block of code extracts all of the G00 and G01 gcode commands and replaces ‘Y’ with ‘Z’, ‘X’ with ‘Y’ and ‘Z’ with ‘X’. We’re also ignoring the gcode feed rate parameter, ‘F’. Then we’re storing the gcode into our list gc. We also append ‘G90’ before every command to ensure that Dorna will move in absolute coordinates.

This will take the content of star_0001.ngc file and output the following:

%
(Header)
(Generated by gcodetools from Inkscape.)
(Using default header. To add your own header create file "header" in the output dir.)
M3
(Header end.)
G20 (All units in inches)

(Start cutting path id: path1009)
(Change tool to Default tool)

G00 Z15.000000
G00 X0.000001 Y11.000000

G01 Z14.500000 F100.0(Penetrate)
G01 X0.881679 Y9.213526 Z14.500000 F400.000000
G01 X2.853170 Y8.927051 Z14.500000
G01 X1.426585 Y7.536475 Z14.500000
G01 X1.763356 Y5.572949 Z14.500000
G01 X0.000001 Y6.500000 Z14.500000
G01 X-1.763355 Y5.572950 Z14.500000
G01 X-1.426584 Y7.536475 Z14.500000
G01 X-2.853169 Y8.927052 Z14.500000
G01 X-0.881677 Y9.213526 Z14.500000
G01 X0.000001 Y11.000000 Z14.500000
G00 Z15.000000

(End cutting path id: path1009)

(Footer)
M5
G00 X0.0000 Y0.0000
M2
(Using default footer. To add your own footer create file "footer" in the output dir.)
(end)
%
['G90',
 'G00 X15.000000',
 'G90',
 'G00 Y0.000001 Z11.000000',
 'G90',
 'G01 X14.500000',
 'G90',
 'G01 Y0.881679 Z9.213526 X14.500000',
 'G90',
 'G01 Y2.853170 Z8.927051 X14.500000',
 'G90',
 'G01 Y1.426585 Z7.536475 X14.500000',
 'G90',
 'G01 Y1.763356 Z5.572949 X14.500000',
 'G90',
 'G01 Y0.000001 Z6.500000 X14.500000',
 'G90',
 'G01 Y-1.763355 Z5.572950 X14.500000',
 'G90',
 'G01 Y-1.426584 Z7.536475 X14.500000',
 'G90',
 'G01 Y-2.853169 Z8.927052 X14.500000',
 'G90',
 'G01 Y-0.881677 Z9.213526 X14.500000',
 'G90',
 'G01 Y0.000001 Z11.000000 X14.500000',
 'G90',
 'G00 X15.000000']

Now we’ll loop through gc and pass the commands into robot.gcode(gc). We can use the json module and robot.command(id = *job id*) to monitor if our last command has completed. Once your script reaches this point, you should see Dorna moving along the path you drew in Inkscape.

for l in gc:
    tmp = {"gc": l}
    job = robot.gcode(tmp)

# Wait for the job to finish
parsed_json = json.loads(job)
job_id = parsed_json[0]['id']
parsed_json = json.loads(robot.command(id = job_id))
print("Working on job id = {}".format(job_id))
while (parsed_json[0]['state'] != 2):
    time.sleep(0.1)
    parsed_json = json.loads(robot.command(id = job_id))
print("Done!\n")

Now that the job is completed, let’s terminate the robot object and end our script.

robot.terminate()

Attatch your light source to Dorna and run the script. You can find more information on light painting and how to set up your camera here.

> python3 LED_Image_Draw.py

homed

The full script is listed below.

from dorna import Dorna
import time
import json

# Connect to Dorna
robot = Dorna()
a = robot.connect()
parsed_json = json.loads(a)

if parsed_json['connection'] == 2:

    print("Connected. Starting job.")

    # REMEMBER TO PLACE ROBOT IN HOME POSITION PHYSICALLY #
    robot.home("j0")
    robot.home("j1")
    robot.home("j2")
    robot.home("j3")

    start  = {"command": "move", "prm": {"path": "joint", "movement": 0, "speed": 1500.0, "j0":0,"j1": 0,"j2": 0,"j3":0,"j4":0}}
    robot.play(start)

    # Get gcode
    with open('star_0001.ngc') as f:
        content = f.readlines()

    gc = []

    for i in range(0,len(content)):
        if (content[i][1:7] == 'Footer'):
            break
        if (content[i][0:3] == 'G00' or content[i][0:3] == 'G01'):
                content[i] = content[i].replace('X','t1').replace('Y','t2').replace('Z','X').replace('t2','Z').replace('t1','Y')
                content[i] = content[i][:content[i].find(' F')]
                gc.append("G90")
                gc.append(content[i])

    # Play the gcode commands
    for l in gc:
        tmp = {"gc": l}
        job = robot.gcode(tmp)

    # Wait for the last job to finish
    parsed_json = json.loads(job)
    job_id = parsed_json[0]['id']
    parsed_json = json.loads(robot.command(id = job_id))
    print("Working on job id = {}".format(job_id))
    while (parsed_json[0]['state'] != 2):
        time.sleep(0.1)
        parsed_json = json.loads(robot.command(id = job_id))
    print("Done!\n")

else:
    print("Error: The robot is not connected.")

time.sleep(5)
robot.terminate()