AxiDraw Server - lurkertech.com
lurkertech.com AxiDraw Server

AxiDraw Server

by Chris Pirazzi and Paul Haeberli

What Is It?

Below we present a tiny Python server script which connects to an AxiDraw plotter plugged into your local computer, listens for connections on the internet, and allows remote clients to send commands to your plotter.

The AxiDraw server is useful if:

The AxiDraw server is not useful if:

Prerequisites

You will need Python3 installed.

Our Python server uses the super-cool "Unofficial Python library" called axi from fogleman at https://github.com/fogleman/axi.

Fogleman's axi library handles the low-level USB location of and communication with the AxiDraw plotter. We use this support to establish a pipe of communication with the plotter.

Fogleman's axi library also has a collection of handy high-level utilities to do path planning and other tasks which you can access as an option (more below).

So, first follow the installation instructions on Fogleman's github page to install the axi package in your copy of Python.

Python axi module Installation tip: the particular MacBook on which I was installing Fogleman's axi package happened to have both python and python3 installed, and this caused huge problems for me because various parts of the installation kept trying to use python instead of python3. While Fogleman's module is downloadable from his github page, his module depends on 4-5 other modules that the installer must download from a central repository, and this failed for me repeatedly. You can tell that some part of the installer is using python (not 3) because you will get inscrutable TLS/SSL errors that make no sense. I had to change various parts of the instuctions by adding 3's everywhere, including using pip3 and I think even modifying one script involved in the installation, and also I had to manually install each of the 4-5 modules you will see mentioned when you try to install, before axi would finally install successfully. It's better if you just don't have pre-3 python at all and if your python3 is called just "python."

You will not need Fogleman's axi command-line program to use our server, but it's a good test to make sure this layer is working ok.

What Commands Can I Send?

This server offers three totally different ways to send commands to AxiDraw:

Method 1: Raw EBB Commands

If you connect to the server and simply start typing stuff, you will be sending raw commands using the surprisingly well-documented EBB command set.

Here are a few examples of EBB commands (see their documentaition for all the details):

V          # get version of AxiDraw firmware
SC p v     # configure params
EM m m     # enable XY motors m=microstepping mode
EM 0 0     # disable XY motors
QM         # motor status (1 somewhere means still moving)
SP 0 delay # pen down
SP 1 delay # pen up
XM duration a b # stepper move
This lets you control the plotter X, Y, and Z position at the most basic level.

Advantage: You have full control of the plotter.

Disadvantage: You have to do all your own path planning for efficient drawing of complex scenes (e.g. slow down around curves, draw lines close together, etc.).

Possible Dangers: You can easily permanently damage your plotter if you tell it to go past its XY or Z (pen height) range, and probably in other ways too. Use at your own risk!

Method 2: Use PATHCMD

Our server has a simple high-level interface that lets you securely send a "drawing" consisting of any number of 2-D strokes of any complexity. Once you finish sending your whole "drawing," our server uses the path planning facilities of Fogleman's axi library to plot your points efficiently and with high quality.

The PATHCMD command has three sub-commands:

PATHCMD drawing_start
PATHCMD stroke <number_of_points>  x y  x y  x y  ...
PATHCMD drawing_end
Each stroke sub-command describes the path of one pen-down-move-move-move-pen-up sequence, with an unlimited number of moves per stroke and an unlimited number of strokes.

Here's an example:

PATHCMD drawing_start
PATHCMD stroke 3 0.1 0.4 0.6 2.3 3.4 2.0
PATHCMD stroke 2 0.1 0.6 2.3 3.4
PATHCMD drawing_end
The x and y coordinates are scaled so that 0.0 maps to coordinate 0 on the printer (closest to the USB connector in both axes) and 1.0 maps to the V3_SIZEX/V3_SIZEY scaling value (in inches) from axiserver.py. You can use coordinates greater than 1.0 and they will simply extrapolate beyond V3_SIZEX/V3_SIZEY. So for example if you have an 8.5 x 11 inch piece of paper and you set V3_SIZEX == V3_SIZEY == 8.5 then you can access the extra 2.5 inches in one dimension by using x values greater than 1.0. You will generally want to set V3_SIZEX == V3_SIZEY unless you use the special NX/NY feature described below.

Advantage: You get the high drawing speed and accuracy benefits of path planning.

Advantage 2: If you issue several drawing_start/drawing_end commands in a row, and you have set the NX and NY parameters in axiserver.py to something other than 1, then axiserver.py will subdivide your paper into sub-rectangles and send each plot to a new sub-rectangle. The 0.0-1.0 values described above get further scaled down to fit the sub-rectangle. This is great when you are debugging some plotting code and you don't want to keep replacing the paper or moving the plotter head. If NX != NY then you will want to set V3_SIZEX/V3_SIZEY accordingly to prevent your drawing from being stretched or squished in one axis. For example, if NX==3 and NY==2 then a reasonable scaling value to keep things in proportion is V3_SIZEX==12 and V3_SIZEY==8.5.

Disadvantage: You don't have as much control as the low-level API (which is useful for things like our 3-D AxiDraw Plotting Hack). You only have access to one small part of what Fogleman's library offers.

Possible Dangers: You can still easily permanently damage your plotter if you tell it to go past its XY or Z (pen height) range, and probably in other ways too. Use at your own risk!

Method 3: Send Python Code! (INSECURE: OFF by default)

Finally, if you want the most flexibility, you can simply send python code sandwiched between two lines containing only a double quote, like this:

"
print("hi")
"
and the server will happily "exec" your code, no matter what it is.

Advantage: You have access to the Fogleman axi object and so you can use any feature of his axi Python library, or for that matter any other library or any facility available to Python!

Disadvantage: You have to figure out the axi library, which is a bit more complex than PATHCMD.

Definite Danger: SUPER SUPER SECURITY HOLE! This feature can only be used on a secure local network where you trust every device on the network. It essentially gives the whole world complete access to your computer without a password! They can do anything a Python script can do, including delete all your files and run programs. For that reason, Method 3 is disabled by default. You can enable the feature by uncommenting the "exec" line in the server. Use at your own risk!

Possible Dangers: You can still easily permanently damage your plotter if you tell it to go past its XY or Z (pen height) range, and probably in other ways too. Use at your own risk!

Running the Server

Just do:

% python3 axiserver.py
By default, the server listens on the highly creative port 12345, but you can change this in the code where it says CHANGE PORT NUMBER HERE.

Simple Client Test with telnet

You can test out the server by simply using your computer's telnet client:

% telnet myserver 12345
You can type commands and see responses immediately.

The Server Code

Code is also available for download here.

#
#       axiserver.py - tiny AxiDraw plot server 
#
#               Chris Pirazzi and Paul Haeberli
#
# For details and instructions, see https://lurkertech.com/axiserver
#
import axi
import sys

device = axi.Device()

# V3_SIZEX and V3_SIZEY
# - this value is NOT the size of your paper
# - this value DOES control how PATHCMD scales x and y to your paper size
# - x,y coordinate values passed to PATHCMD get mapped into to this range
#   - with 0.0 mapped to 0 (closest to the USB connector in both axes)
#   - and  1.0 mapped to V3_SIZEX or V3_SIZEY
# - you can use coordinates >1.0 no problem
#   - they simply extrapolate beyond V3_SIZEX or V3_SIZEY
# - so with an 8.5 x 11 inch paper you might choose V3_SIZEX==V3_SIZEY==8.5
#   - with x coordinates > 1.0 to reach the extra 2.5 inches
# - it's important that V3_SIZEX==V3_SIZEY if NX==NY==1
#   - otherwise your PATHCMD drawing will be stretched or squished in one axis
# - if you set NX and NY to values other than 1
#   - V3_SIZEX and V3_SIZEY must have the opposite proportion to avoid squishing
# - for more details, see https://lurkertech.com/axiserver
#
V3_SIZEX = 8.5
V3_SIZEY = 8.5

# - sets the division of the plotter surface
# - each new plot goes into the next place on the paper
#   - helpful when you're debugging some plotting code
#   - lets you avoid constantly putting new paper or manually moving the pen
# - to plot on the whole surface set these to 1
# - if these are 1, V3_SIZEX should equal V3_SIZEY
# - if these are not 1, V3_SIZEX/V3_SIZEY must stay in proportion
#   - for more details, see https://lurkertech.com/axiserver
#
NX = 1
NY = 1
PADDING = 0.1

# how the plot area is divided
CELLSIZEX = V3_SIZEX/NX
CELLSIZEY = V3_SIZEY/NY
POSX = 0
POSY = 0

def nextpos():
    global POSX
    global POSY
    POSX = POSX+1;
    if(POSX == NX):
        POSX = 0
        POSY = POSY+1
        if(POSY == NY):
            POSY = 0

def goodbuf(buf): 
   if not buf: return False
   if buf[0] == 0xff: return False
   if buf == '': return False
   return True

sofar = ""

def recv(connection):
    global sofar
    while True:
        off = sofar.find("\n");
        if -1 != off: break
        #print("reading more from connection")
        buf = connection.recv(1024)
        if not buf: return None
        if buf[0] == 0xff: return None
        if buf == '': return None
        buf = buf.decode("utf-8")   
        #print("read [" + buf + "] from connection")
        sofar += buf
    ret = sofar[0:off];
    ret = ret.rstrip()
    sofar = sofar[off+1:]
    #print("remaining sofar is [" + sofar + "]")
    #print("returning [" + ret + "] to caller")
    return ret;

curpath = []
paths = []

def stroke_start():
    global curpath
    curpath = []

def stroke_addpoint(x, y):
    global curpath
    curpath.append((x, y))

def stroke_end():
    global paths
    global curpath
    paths.append(curpath)

def drawing_start():
    print("DRAWING START")
    global paths
    paths = []

def drawing_end():
    global paths
    d = axi.Drawing(paths)
    d = d.scale(1.0, -1.0)
    d = d.scale_to_fit(CELLSIZEX, CELLSIZEY, PADDING)
    d = d.translate(POSX*CELLSIZEX,  POSY*CELLSIZEY)
    axi.draw(d)
    nextpos()
    print("DRAWING END")
    print("")

def pathcmd(*ary):
    print(ary)
    if ary[1] == "drawing_start":
        print("PCMD: start")
        drawing_start()
    elif ary[1] == "drawing_end":
        print("PCMD: end")
        drawing_end()
    elif ary[1] == "stroke":
        print("PCMD: stroke")
        npoints = ary[2]
        print("npoints" + npoints)
        stroke_start()
        for i in range(int(npoints)):
            x = float(ary[2*i+3])
            y = float(ary[2*i+4])
            print("pointx: " + str(x) + "pointy: " + str(y))
            stroke_addpoint(x, y)
        stroke_end()
    else:
        print("strange PATHCMD: " + ary[1])
    

import socket,os
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
sock.bind(('', 12345))  # CHANGE PORT NUMBER HERE!
sock.listen(5)  
while True:  
    connection,address = sock.accept()
    sendit = lambda string: connection.send(string.encode("utf-8"))
    #print("got connection") 
    sofar = "";
    while True:
        buf = recv(connection)
        if buf is None: break
        source = ""
        if(buf == "\""):
            #print("starting some python code")
            while True:
                buf = recv(connection)
                if buf is None: break
                if(buf == "\""):
                    #print("doing exec of:\n" + source)
                    # DANGEROUS on public networks!!!!
                    # uncomment for python interface
                    #exec(source)
                    break
                source += buf + "\n"
        if source != "": continue
        if buf is None: break
        ary = buf.split(" ")
        #print("cmd ary:")
        print(ary)
        print(ary[0])
        if ary[0] == "PATHCMD":
            pathcmd(*ary)
        else:
            response = device.command(*ary)
            print("device response: " + response)
            response += "\n"
            response = response.encode('utf-8')
            connection.send(response)    
    #print("connection closed")
    connection.close()

C Client Code

If your client is in C you can use this code to connect to the server, send commands, and receive responses.

This is basically just boilerplate C socket client code with some examples of sending commands using the two higher-level APIs above:

Code is also available for download here.

//
//      axiclient -
//
//              Chris Pirazzi and Paul Haeberli
//
//      Demo C code to send path to axidraw plotter over the net.
//
// For details and instructions, see https://lurkertech.com/axiserver
//
#include 
#include 
#include 
#include 
#include 
#include  

static char axidraw_buffer[BUFSIZ];
static int axidraw_sockfd;

static void axidraw_send_command(const char *str)
{
    int str_len = strlen(str);
    if (write(axidraw_sockfd, str, str_len) == -1) {
        perror("write");
        return;
    }
}

static void axidraw_read_response(int echo)
{
    ssize_t nbytes_read;

    if ((nbytes_read = read(axidraw_sockfd, axidraw_buffer, BUFSIZ)) > 0) {
        if (echo) {
            write(STDOUT_FILENO, axidraw_buffer, nbytes_read);
            if (axidraw_buffer[nbytes_read - 1] == '\n')
                fflush(stdout);
        }
    }
}

static int axidraw_init()
{
    char protoname[] = "tcp";
    struct protoent *protoent;
    in_addr_t in_addr;
    struct hostent *hostent;
    struct sockaddr_in sockaddr_in;
    const char *axidraw_hostname = "studio-muji.local";
    unsigned short axidraw_port = 12345;

    protoent = getprotobyname(protoname);
    if (protoent == NULL) {
        perror("getprotobyname");
        return 0;
    }
    axidraw_sockfd = socket(AF_INET, SOCK_STREAM, protoent->p_proto);
    if (axidraw_sockfd == -1) {
        perror("socket");
        return 0;
    }

    /* Prepare sockaddr_in. */
    hostent = gethostbyname(axidraw_hostname);
    if (hostent == NULL) {
        fprintf(stderr, "error: gethostbyname(\"%s\")\n", axidraw_hostname);
        return 0;
    }
    in_addr = inet_addr(inet_ntoa(*(struct in_addr*)*(hostent->h_addr_list)));
    if (in_addr == (in_addr_t)-1) {
        fprintf(stderr, "error: inet_addr(\"%s\")\n", *(hostent->h_addr_list));
        return 0;
    }
    sockaddr_in.sin_addr.s_addr = in_addr;
    sockaddr_in.sin_family = AF_INET;
    sockaddr_in.sin_port = htons(axidraw_port);

    /* Do the actual connection. */
    if (connect(axidraw_sockfd, (struct sockaddr*)&sockaddr_in, sizeof(sockaddr_in)) == -1) {
        perror("connect");
        return 0;
    }
    return 1;
}

static void axidraw_close()
{
    close(axidraw_sockfd);
}

#ifdef USE_PYTHON_INTERFACE // insecure Method 3
//
// PYTHON axidraw commands to define a drawing
// - these use insecure Method 2
// - so you would need to uncomment the 'exec' in axiserver.py to use these
// - it's probably better to use PATHCMD as shown below
//
static void py_stroke_start()
{
    axidraw_send_command("\"\n");
    axidraw_send_command("stroke_start()\n");
}

static void py_stroke_addpoint(float x, float y)
{
    char cmdstr[1024];
    sprintf(cmdstr, "stroke_addpoint(%f,%f)\n", x, y);
    axidraw_send_command(cmdstr);
}

static void py_stroke_end()
{
    axidraw_send_command("stroke_end()\n");
    axidraw_send_command("\"\n");
}

static void py_drawing_start()
{
    axidraw_send_command("\"\n");
    axidraw_send_command("drawing_start()\n");
    axidraw_send_command("\"\n");
}

static void py_drawing_end()
{
    axidraw_send_command("\"\n");
    axidraw_send_command("drawing_end()\n");
    axidraw_send_command("\"\n");
}
#endif // USE_PYTHON_INTERFACE // insecure Method 3

int main()
{
    if (!axidraw_init()) {
        exit(1);
        return 1;
    }

#ifdef USE_PYTHON_INTERFACE // insecure Method 3
    //
    // here we send commands in python to the server.
    // WARNING: this may make your host vulnerable! 
    // you need to uncomment the "exec" in server.py
    // for this to work.
    //
    py_drawing_start();
        py_stroke_start();
        py_stroke_addpoint(0.0, 0.0);
        py_stroke_addpoint(1.0, 0.0);
        py_stroke_addpoint(1.0, 1.0);
        py_stroke_addpoint(0.0, 1.0);
        py_stroke_addpoint(0.0, 0.0);
        py_stroke_end();
    py_drawing_end();
#endif // USE_PYTHON_INTERFACE // insecure Method 3

#define USE_PATHCMD_INTERFACE

#ifdef USE_PATHCMD_INTERFACE // Method 2
    //
    // here we send PATHCMD commands to the server
    //
    axidraw_send_command("PATHCMD drawing_start\n");
    axidraw_send_command("PATHCMD stroke 5 0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0 0.0 0.0\n");
    axidraw_send_command("PATHCMD stroke 5 0.2 0.2 0.8 0.2 0.8 0.8 0.2 0.8 0.2 0.2\n");
    axidraw_send_command("PATHCMD drawing_end\n");
#endif // USE_PATHCMD_INTERFACE // Method 2

    axidraw_close();
    exit(0);
    return 0;
}

Perl Client Code

If your client is in Perl, you can find client code to connect to our server on the page with our 3-D AxiDraw Plotting Hack.

Clients in Other Languages

Simply Google the name of your programming language and "simple TCP client" or "simple socket open code." You want to open the host running the server on port 12345 (or whatever number you provided in axiserver.py) and then send commands as seen in the other sample code above.

Comparison with ssh

You can also share an AxiDraw device by opening up ssh (secure remote shell) on the computer physically connected to the device and allowing people to log into your computer and run whatever script.

ssh Advantages: Secure connection method that works over the general internet and on untrusted networks. Lets client do pretty much anything on the server, but only if they have proper credentials. No need for a custom server program.

ssh Disadvantages: Making an ssh connection from client C/Python/Perl/... code is much much more code (not just the 30-line samples you see above), unless the client uses their programming language's "system()/popen()" call to run a command-line ssh client, which may be inefficient depending on the application. The client gets complete access to the server (can run any program, delete files accessible by the ssh user), so the owner of the server needs to trust the client user more than with Method 1 and Method 2 above (but less than with Method 3!). It is possible to limit impact somewhat by creating a separate user just for this purpose. The client cannot use the high-level functionality of PATHCMD (including its ability to put multiple drawings on one page in sub-rectangles) but on the other hand it has access to a world of ready-made scripts to do things with AxiDraw.

3-D AxiDraw Plotting Hack

If you found this interesting, you might like to check out our 3-D AxiDraw Plotting Hack, which lets you continuously move your plotter pen in X, Y, and Z (pen height) simultaneously to create interesting artistic effects with a brush pen.

Submit This SiteLike what you see?
Help spread the word on social media:
Support This SiteThis free site is supported by reader contributions. You can contribute in the following ways:
donate now   Donate Now
Use your credit card or PayPal to donate in support of the site.
get anything from amazon.com
Use this link to Amazon—you pay the same, I get 4%.
get my thai dictionary app
Learn Thai with my Talking Thai-English-Thai Dictionary app: iOS, Android, Windows.
get my thai phrasebook app
Experience Thailand richly with my Talking Thai-English-Thai Phrasebook app.
get my chinese phrasebook app
Visit China easily with my Talking Chinese-English-Chinese Phrasebook app.
get thailand fever
I co-authored this bilingual cultural guide to Thai-Western romantic relationships.
CopyrightAll text and images copyright 1999-2023 Chris Pirazzi unless otherwise indicated.
Submit This Site

Like what you see?
Help spread the word on social media:
Support This Site

This free site is supported by reader contributions. You can contribute in the following ways:
donate now   Donate Now
Use your credit card or PayPal to donate in support of the site.
get anything from amazon.com
Use this link to Amazon—you pay the same, I get 4%.
get my thai dictionary app
Learn Thai with my Talking Thai-English-Thai Dictionary app: iOS, Android, Windows.
get my thai phrasebook app
Experience Thailand richly with my Talking Thai-English-Thai Phrasebook app.
get my chinese phrasebook app
Visit China easily with my Talking Chinese-English-Chinese Phrasebook app.
get thailand fever
I co-authored this bilingual cultural guide to Thai-Western romantic relationships.
Copyright

All text and images copyright 1999-2023 Chris Pirazzi unless otherwise indicated.