TITLE:  Open-source Toolkit ( OSTK ) for real-time CNC control

CATEGORY:   Graduate Thesis Project, Software Design, Industrial Design, Fabrication

DATE:   June, 2016

TOOLKITS:   Cinder, C++, Solidworks

COLLABORATORS:   N/A

DESCRIPTION:
Tisch's Interactive Telecommunications Program ( ITP ), the program in which I attained my Master's degree, provided me with a plethora of incredible tools with which I was able to fully explore my creativity and bring to fruition numerous speculative project ideas.  These tools for fabrication included laser-cutters, 3D printers and 3-axis mills. I was fortunate, given my training as an Industrial designer, that I was taught the skills necessary to take full advantage of these machines; in particular certain design and 3D CAD (computer-aided design) software packages.  However, the barrier for entry for taking full advantage of these machines is still relatively high. Users often need to learn complex design software packages necessary for generating content, along with the fact that most machines make use of proprietary firmware - limiting the use of the machines to the software shipped with them.  My graduate thesis project focused on lowering this barrier for entry, and making the hardware and software more available to the everyman by opensourcing it.  The final project consists of an open-source design for 2-axis pen plotter, custom communication protocol for controlling it, as well easy-to-use software with a simple graphical user intefrace ( GUI ).

CODE:  https://github.com/Craigson/SketchCNC

URL:   https://itp.nyu.edu/thesis2016/project/craig-pickard


THESIS FINAL PROJECT PRESENTATION:
The video shown below is a recording of my final thesis project presentation.  We were required to present the overarching concept and outline our process in a 10 minute presentation that was streamed lived over the internet.  The presentation includes a live demo of the plotter in action. This video may quite possibly be a case of TL;DR, so the important points are broken out and outlined in the sections that follow.


BACKGROUND:
Computer Numerical Control ( CNC ) is a communication protocol used for interfacing with machinery used for fabrication; machines like 3D printers, laser cutters, routers and mills.  These can be, and often are, referred to using the generic term 'CNC machines'.  Traditionally a 2D or 3D file is created using a piece of computer-aided design ( CAD ) software, and is then converted into a set of machine-readable instructions before the part can be produced.  These instructions are then executed by the machine making use of the aforementioned CNC protocol.  The problem with this, in my opinion, is that it offers no form of interaction - there is no feedback loop.  As such, my thesis project explored the idea of using the familiar metaphor of drawing as the primary method of interaction, meaning no prior expertise or knowledge of software was required to operate the machine; only the ability to hold a pen.

I designed and built a custom 2-axis pen plotter to demonstrate and explore this idea.  This would allow me to write my own software, along with my own communication protocol.  There were several motivating factors behind the decision to do this, namely: I wouldn't have to spend a relatively large sum of money to procure such a machine, I wouldn't be limited to the software with which it's intended to be used, and I'd have full control over the user-interaction.


INTERFACE DESIGN and USER-INTERACTION:
The main motivator for doing this project was creating a mode of interaction that would allow anyone, even a child, to control a CNC machine ( and in this case, for the purpose of demonstration, a simple 2-axis pen plotter ).  This is the precise reason I selected drawing as my metaphor for interaction, as it's a skill we're all taught at a very early age.  By making use of a drawing tablet and stylus, I could take the user's pen strokes as digital input and send those, in real-time, to the plotter.  I wanted to focus on an interface that was simple, uncluttered, and similar enough to existing digital drawing and design applications such as Adobe Illustrator.  I used Cinder, a C++ framework for creative coding, to build the application - making use of the fantastic ImGui library for the design of the interface.

Screenshot of the prototype Graphical User Interface ( GUI ) showing three simple tools: the pencil, the line, and the circle.

The majority of user-testing took place at ITP's annual Spring Showcase.  The event is open to the public and provides students with a good platform for displaying their work, a well as gathering valuable feedback from users not necessarily very familiar with the technology involved.  I setup a Wacom Cintiq tablet alongside the plotter and allowed people to use the application to explore the interface without any instructions.  The images below were collected over the two days of the showcase.


SOFTWARE DESIGN, HARDWARE and COMMUNICATION PROTOCOL:
I owe a great debt of gratitude to Brian Schmalz, the designer of the EiBotBoard ( sold by EvilMadScientist ) - the motor control board I used for the plotter. He graciously donated his time to help me debug portions of the code and troubleshoot problems with the design of my communication protocol.

 The EiBotBoard v2.0, the motor control board for the plotter

The EiBotBoard v2.0, the motor control board for the plotter

The EiBotBoard hardware consists of a PIC18F46J50 microcontroller and two Allegro A4983 stepper drivers, along with some voltage regulators and USB connection hardware.  It interfaces with the custom software I wrote via the Serial Port and communicates over USB.  The EiBotBoard's firmware is designed to take string commands using serial communication ( the process of sending data sequentially bit by bit over a single channel ).

 Screenshot of the communication library created using the Cinder C++ framework

Screenshot of the communication library created using the Cinder C++ framework

The key to the real-time control of the plotter lay in the call-and-response communication style. Sending too many sequential commands over Serial to the EiBotBoard caused the buffer to overflow, which in turn lead to several commands failing to execute.  The custom protocol waits for a response from the EiBotBoard before sending the next command off of the top of the 'queued commands' stack.  Adding complications to this process was the fact that the software had to send commands to the limit switch for each axis to ensure that the gantry was in the correct position.  Because every command is executed synchronously, this meant that waiting for the response from each limit switch had to be carefully managed before sending any movement commands.

The snippet below shows the code necessary to return the printhead to the origin:

/**********************************************************************
 *               M O V E   T O   O R I G I N
 ***********************************************************************/
void PlotBot::moveToOrigin()
{
    std::cout<< std::endl << "----------------------------------------------------------------" << std::endl << std::endl;
    std::cout << "frame number: " << ci::app::getElapsedFrames() << std::endl << std::endl;
    //IF BOTH THE X AND Y-AXES ARE AT HOME, THEN THERE'S NO LONGER A NEED TO HOMING, SET THE LOCATION TO 0,0
    if (xHome && yHome)
    {
        xHome = false;
        yHome = false;
        mOperationMode = NORMAL_OPERATION;
        mPenPixelPosition = ci::vec2(0);
        return;
    }
    if (mBoard->mSerial->getNumBytesAvailable() > 0)
    {
        std::cout<< "CHECK ------ " << std::endl;
        auto numBytes = mBoard->mSerial->getNumBytesAvailable();
        uint8_t packet[numBytes];                               //CHECK HOW MANY BYTES ARE AVAILABLE
        mBoard->mSerial->readBytes(packet, numBytes);                   //READ AVAILABLE BYTES AND PLACE THEM IN 'PACKET' BUFFER
        //THE CURRENT SWITCH STATE DEPENDS ON WHICH CORRESPONDING PIN-MOTOR PAIR IS BEING ADDRESSED
        switch (mAxisState) {
                std::cout << "bytes received dawg, now checking which axis is engaged.." << std::endl;
            case X_AXIS:
                std::cout << "X-Axis engaged in check mode" << std::endl;
                switch (mHomingMode)
            {
                case CHECK_LIMIT_RESPONSE_X:
                    std::cout << "CHECK_LIMIT_RESPONSE_X activated..." << std::endl;
                    if (numBytes == 6)
                    {
                        std::string packetString = std::to_string(packet[3]);    //THIS HANDLES THE REQUEST FOR THE PIN STATE
                        std::cout << "numBytes: " << numBytes << " , packet: " << packetString << std::endl << std::endl;
                        
                        if (packetString.compare("48") == 0)
                        {
                            std::cout << "X-Axis gantry has reached the origin, switching axis state to Y-AXIS and mode to REQUEST_LIMIT_Y" << std::endl << std::endl;
                            xHome = true;
                            mAxisState = Y_AXIS;
                            mHomingMode = REQUEST_LIMIT_STATE_Y;
                        }
                        if (packetString.compare("49") == 0)
                        {
                            std::cout << "We're not home yet, switching mode to MOVE_MOTOR_X" << std::endl;
                            mHomingMode = MOVE_MOTOR_X;
                        }
                        
                    }
                    break;
                case CHECK_MOTOR_RESPONSE_X:
                    std::cout << "CHECK_MOTOR_RESPONSE_X activated..." << std::endl;
                    std::cout << "numBytes: " << numBytes << ", packet: " << packet << std::endl;
                    
                    if (numBytes == 4)
                    {
                        std::string packetString = std::to_string(packet[2]) + std::to_string(packet[3]);
                        
                        std::cout << "packet string is: " << packetString << std::endl;
                        
                        //IF THE RESPONSE PACKET INDICATES THAT THE MOTOR SUCCESSFULLY MOVED THE X-AXIS, IT'S
                        //TIME TO CHECK THE STATE OF THE Y-AXIS LIMIT SWITCH
                        if (packetString.compare("1310") == 0 && !yHome)
                        {
                            std::cout << "motor response for x-axis was good, switching to y-axis and mode to REQUEST_LIMIT_STATE_Y" << std::endl;
                            mAxisState = Y_AXIS;
                            mHomingMode = REQUEST_LIMIT_STATE_Y;
                            
                        } else if (packetString.compare("1310") == 0 && yHome)
                        {
                            std::cout << "motor response for x-axis was good, but the y-axis is home, so let's stay on the X-AXIS and switch mode to REQUEST_LIMIT_STATE_X" << std::endl;
                            mHomingMode = REQUEST_LIMIT_STATE_X; 
                        }
                        //                                else {
                        //                                    mHomingMode = REQUEST_LIMIT_STATE_X; //this might need to change to send motor command again
                        //                                }
                    }
                    break; 
                default:
                    break;
            } //END OF mHomingMode SWITCH STATEMENT
                break;
            case Y_AXIS:
                std::cout << "Y-Axis engaged in check mode" << std::endl;
                switch(mHomingMode)
            {  
                case CHECK_LIMIT_RESPONSE_Y:
                    if (numBytes == 6)
                    {
                        std::string packetString = std::to_string(packet[3]);    //THIS HANDLES THE REQUEST FOR THE PIN STATE
                        std::cout << "numBytes: " << numBytes << " , packet: " << packetString << std::endl << std::endl;
                        
                        //IF THE Y-AXIS LIMIT-SWITCH STATE IS LOW, THE DRAWING HEAD HAS REACHED THE Y-AXIS ORIGIN
                        if (packetString.compare("48") == 0)
                        {
                            std::cout << "Y-Axis gantry has reached the origin, switching axis state to X-AXIS and mode to REQUEST_LIMIT_X" << std::endl << std::endl;
                            yHome = true;
                            mAxisState = X_AXIS;
                            mHomingMode = REQUEST_LIMIT_STATE_X; 
                        }
                        if (packetString.compare("49") == 0)
                        {
                            std::cout << "We're not home yet, switching mode to MOVE_MOTOR_Y" << std::endl;
                            mHomingMode = MOVE_MOTOR_Y;
                        }  
                    }
                    break;
                case CHECK_MOTOR_RESPONSE_Y:
                    //IF THE RESPONSE PACKET INDICATES THAT THE MOTOR SUCCESSFULLY MOVED ALONG THE Y-AXIS, IT'S
                    //TIME TO CHECK THE STATE OF THE X-AXIS LIMIT SWITCH
                    if (numBytes == 4)
                    {
                        std::string packetString = std::to_string(packet[2]) + std::to_string(packet[3]);

                        //IF THE RESPONSE PACKET INDICATES THAT THE MOTOR SUCCESSFULLY MOVED THE X-AXIS, IT'S
                        //TIME TO CHECK THE STATE OF THE Y-AXIS LIMIT SWITCH
                        if (packetString.compare("1310") == 0)
                        {
                            std::cout << "motor response was good, x-axis is home though, so no need to switch, let's switch mode to REQUEST_LIMIT_STATE_Y" << std::endl;
                            mHomingMode = REQUEST_LIMIT_STATE_Y;
                        } 
                    }
                    break;   
                default:
                    break; 
            } //END OF mHomingMode SWITCH STATEMENT
                break;
            default:
                break;
        }   //END OF AXIS_MODE SWITCH STATEMENT
        mBoard->flushBuffer();
    } //END OF CHECK BYTES AVAILABLE
        std::cout << "SEND ------" << std::endl;
        //THE CURRENT SWITCH STATE DEPENDS ON WHICH CORRESPONDING PIN-MOTOR PAIR IS BEING ADDRESSED
        switch (mAxisState) {

            case X_AXIS:
                std::cout << std::endl << "we're about to send a command... for the x-axis" << std::endl;
                switch (mHomingMode)
            {
                case REQUEST_LIMIT_STATE_X:
                    if(!xHome) //ONLY SEND THE REQUEST IF THE GANTRY IS NOT AT HOME
                    {
                        std::cout << "Sending pin-state request for X and switching mode to CHECK_LIMIT_RESPONSE_X" << std::endl;
                        mBoard->sendCommand("PI,A,2\r");
                        mHomingMode = CHECK_LIMIT_RESPONSE_X;
                    }
                    break;
                case MOVE_MOTOR_X:
                    if (!xHome) //ONLY MOVE THE MOTOR IF THE GANTRY IS NOT AT HOME
                    {
                        std::cout << "Moving x-axis 1mm and switching mode to CHECK_MOTOR_RESPONSE_X" << std::endl;
                        mBoard->sendCommand("SM,50,-100,0\r"); //this moves the x-axis 1mm toward the origin in 30ms
                        mHomingMode = CHECK_MOTOR_RESPONSE_X;
                        std::cout << "homing mode is now: " << mHomingMode << std::endl;
                    }
                default:
                    break;
            }
                break;
            case Y_AXIS:
                std::cout << "we're about to send a command... for the y-axis" << std::endl;
                switch(mHomingMode)
            { 
                case REQUEST_LIMIT_STATE_Y:
                    if(!yHome) //ONLY SEND THE REQUEST IF THE GANTRY IS NOT AT HOME
                    {
                        std::cout << "Sending pin-state request for Y and switching mode to CHECK_LIMIT_RESPONSE_Y" << std::endl;
                        mBoard->sendCommand("PI,A,1\r");
                        mHomingMode = CHECK_LIMIT_RESPONSE_Y;
                    }
                    break;
                case MOVE_MOTOR_Y:
                    if (!yHome) //ONLY MOVE THE MOTOR IF THE GANTRY IS NOT AT HOME
                    {
                        std::cout << "Moving y-axis 1mm and switching Mode to CHECK_MOTOR_RESPONSE_Y" << std::endl;
                        mBoard->sendCommand("SM,50,0,-100\r"); //this moves the x-axis 1mm toward the origin in 30ms
                        mHomingMode = CHECK_MOTOR_RESPONSE_Y;
                    }
                    break;
                default:
                    break;   
            } //END OF mHomingMode SWITCH STATEMENT  
            default:
                break;
        } //END OF mAxisMode SWITCH STATEMENT 
}

PLOTTER DESIGN AND FABRICATION:
Prior to coming to ITP my focus was design-for-fabrication, so I spent a great deal of time using CAD packages like Solidworks.  This experience allowed me to design parts in 3D, create virtual assemblies, and finally produce 2D DXF files for laser-cutting acrylic and 3D printable STL files.   I sourced all the mechanical components from ServoCity, a truly fantastic resource for any hobbyist or maker looking to build anything that moves.  The best part being that they offer downloadable versions of all of the parts they offer, meaning I could include them in my virtual assembly and design my own components around them.  This became a crucial part of my design workflow.


ADDITIONAL PLOTTER FEATURES:
Although the plotter was designed primarily to showcase the real-time control aspect of the interaction, I wanted to include additional features in the software - both in order to channel myself, but also to not limit the functionality and machine's flexibility.  I created two additional 'modes', or features, within the GUI.  The first allows the user to capture an image of themselves using the computer's webcam, and uses a custom stippling algorithm to generate a dot-matrix-style print.  The second feature, given the open-source nature of the software, allows a more knowledgeable user to manipulate the source code and generate patterns algorithmically that can then be executed by the plotter. 
 

Plotter features: real-time control, stippling print algorithm, and generative design and printing

 A custom stippling algorithm takes input from a webcam and generates a printable image.

A custom stippling algorithm takes input from a webcam and generates a printable image.

 Printing generative designs created with code.

Printing generative designs created with code.


FINAL THOUGHTS:
On a personal level I consider the project to be a great success.  I was particularly pleased with the response from users at the ITP spring showcase, especially the kids.  It reinforced my assertion that the interface was both intuitive, and easy to use, and that it got people excited about the possibility of interacting with other machinery this way. I also learnt a great deal, and apart from producing a fully functioning 2-axis plotter, I had the opportunity to dive deeper into Cinder and C++, which was one of my primary goals for my thesis project.

The main point of contention seemed to be around my choice of creating a 2-axis pen plotter.  The main point being raised was "why use a machine to draw on a piece of paper when you could just do the drawing yourself?"  This is a fair point and it's understandable why several people posed the question.  My reasons for doing so were two-fold: The eventual size of the plotter was due to limited resources ( mainly time and money ).  Had I had a larger budget, and more time ( I spent around $300 building the plotter, and the whole project spanned 14 weeks, which included time spent on other classes), I would've created a much larger machine.  Also, given that I'd never designed or built a machine like this, or written software to control it, it seemed like a wise choice aiming for something more achievable than something like a 3D printer.  The project was intended as a proof-of-concept, for the mode of interaction, for the interface and communication protocol, and on all of these fronts I consider it a success.