Author: Mugabi Siro

Category: LinuxCNC

Summary:

This entry discusses CMS/NML interfaces and general considerations when implementing a LinuxCNC user interface that supports streaming of MDI commands from an external program. References to LinuxCNC sources and binaries are with respect to the root of the source tree. LinuxCNC v2.8.0~pre1 git is considered.

Tags: linux linuxcnc realtime

Table Of Contents

Background and Audience

The LinuxCNC package already includes a variety of User Interfaces (UI) ranging from simple text-based programs to high-end graphical interfaces. Along with support for the standard modes of LinuxCNC execution (Manual, Manual Data Input (MDI), and Auto)1, these interfaces include varying degrees of support for monitoring LinuxCNC runtime status info and error messages, tool trajectory plot previews, GUI extension via plugin-modules, etc. A complete GUI such as bin/axis (an OpenGL-based UI written in Python/C++) will include widgets and menu options for operations on most, if not all, LinuxCNC supported machine types and configurations: ranging from 3D printers, industrial lathes/mills and similar machines with up to 9-axes, to different robot manipulator configurations -- serial (e.g. PUMA and SCARA) and parallel (e.g. Stewart/hexapod platforms) -- with nontrivial kinematics.

Nevertheless, the need might arise for a special purpose UI that implements support for a specific/custom setup. Consider the case where it is desirable to integrate the LinuxCNC infrastructure for robot manipulator control into a development framework for robotics, e.g. to implement a Task-level-programming (TLP) system for a robotic Checkers/Chess player. A basic implementation would have an external program issue high-level commands, e.g. pick_from_12( ), according to output of the game engine. This high-level function would then stream a set of pre-tested MDI commands which, in turn, would result in robot manipulator action of picking up a piece from the specified location on the checkers board. For a setup like this, it is quite likely that the implementation of a thin layer will be required to enable direct and "on-line" streaming of MDI commands to the LinuxCNC instance.

For applications that require AUTO mode, it probably makes more sense to stick with the standard and complete LinuxCNC UI programs such as AXIS, Touchy, etc. Also note that the implementation details discussed here describe the low-level C++ interfaces for the Communication Management System (CMS) and Neutral Message Language (NML) frameworks. For simpler (command-line) MDI mode testing, consider the bin/mdi Python module instead.

The CMS/NML (and core LinuxCNC) code base largely derives from work by Fred Proctor and Will Shackleford of U.S. NIST. See docs/src/common/emc-history.txt.

LinuxCNC UI Overview

Generally, a UI program will send control commands to a LinuxCNC instance, and retrieve error messages and status information from it. Control commands include toggling machine state, switching between LinuxCNC execution modes, and motion control. With respect to motion control, the UI provides an interface for G code. There are several dialects of G code. LinuxCNC supports RS274/NGC. G code programs are interpreted. Commands can be executed in MDI mode (entered manually, line-by-line), or in AUTO mode (run as program in a text file).

Sending control commands and retrieving error and status info is done via NML channels. The UI program instantiates separate NML channels for communication of command, error and status info. Sending commands is done by copying data to the respective CMS/NML buffer. On the other hand, retrieving error and status info is typically by way of periodically polling the respective CMS/NML buffers. A CMS/NML buffer is an abstraction for message passing that manages the underlying details of (OS dependent) Inter Process Communication (IPC): In this case, between the UI and the LinuxCNC-core. Section Accessing CMS/NML Buffers discusses CMS/NML buffer access in more detail. The more commonly used IPC mechanisms by LinuxCNC include UNIX/Linux SHared MEMory (SHMEM)2 for LinuxCNC setups on a single computer, and TCP sockets for remote connections between a UI instance and the LinuxCNC-core. Check out LinuxCNC NML Config, HowTo for a more detailed discussion.

An assortment of LinuxCNC command API is available for a UI implementation. This probably stems from LinuxCNC's varied capability (control of various machine types/configurations, and miscellaneous I/O operations such as tool selection). Fortunately, it is possible for the UI developer to restrict API usage to a small subset that is relevant for the task at hand. In addition, there also exist a few libraries which export high-level API to facilitate NML channel creation, sending commands, monitoring error and status info, and other miscellaneous operations such as parsing INI files and performing conversions between measurement units (e.g. inches and millimeters).

The general flow in this entry will be that followed by the ne_linuxcnc_ui_puma.cc implementation. It is a basic (command-line) UI program that is meant to serve as a template for interfacing a LinuxCNC instance with an external program that streams MDI commands. It relies on the src/emc/usr_intf/shcom.cc source code library (by Eric H. Johnson) for the brunt of CMS/NML buffer handling.

Parsing the INI File

The lib/linuxcncini.so library, part of the NML framework, exports a class IniFile object (see include/inifile.hh) to facilitate parsing INI files. The class defines methods such as Open, Close and Find (see src/libnml/inifile/inifile.cc). The IniLoad() library function (shcom.cc) can be used to parse an INI file.

However, note that this library function only includes generic support for parsing the more commonly used INI file settings since the number of possible combinations of INI file settings (arising from the diversity of user requirements) cannot be realistically anticipated by a (single) library function. Should a specific LinuxCNC setup require processing of INI file options that are not parsed by this library function, then the setup's UI may have to implement its own version of IniLoad(). Fortunately, this mainly translates to simply using additional IniFile::Find() methods to allow parsing what is "desirable". For example (See ini_load() in ne_linuxcnc_ui_puma.cc):

// Func : ini_load
// Desc : parse INI file
// NOTES: doing this here, rather than `src/emc/usr_intf/shcom.cc:IniLoad()`,
//        to allow parsing what is "desirable".
static int ini_load(const char *filename)
{
    IniFile inifile;
    ...
    if (!inifile.Open(filename))
        // handle error
    ...
    if (NULL != (inistring = inifile.Find("NML_FILE", "EMC"))) {
        // copy to global
        strcpy(emc_nmlfile, inistring);
    ...
    // establish number of axes/joints
    if (NULL != (inistring = inifile.Find("AXES", "TRAJ"))) {
        emc_axis_cnt = atoi(inistring);
    ...
    // get display cycle time
    if (NULL != (inistring = inifile.Find("CYCLE_TIME", "DISPLAY"))) {
        if (1 != sscanf(inistring, "%lf", &emc_display_cycle)) {
            emc_display_cycle = DEFAULT_DISPLAY_CYCLE;
    ...
    inifile.Close();
    return 0;
}

Instantiating NML Channels

The different kinds of information -- control commands, status info, and error messages (as well as tool commands and status info) -- require separate NML communication channels. For instance, the tryNml() library function (shcom.cc) instantiates the following classes to create NML channels:

  • Control commands:

    RCS_CMD_CHANNEL *emcCommandBuffer;
    ...
    emcCommandBuffer =
        new RCS_CMD_CHANNEL(emcFormat, "emcCommand", "xemc", emc_nmlfile);
    
  • Status info:

    RCS_STAT_CHANNEL *emcStatusBuffer;
    ...
    emcStatusBuffer =
        new RCS_STAT_CHANNEL(emcFormat, "emcStatus", "xemc", emc_nmlfile);
    
  • Error messages:

    NML *emcErrorBuffer;
    ...
    emcErrorBuffer =
        new NML(nmlErrorFormat, "emcError", "xemc", emc_nmlfile);
    

Recall that the "xemc" string denotes the value to use for the Name field in the Process Table of the .nml configuration file. See LinuxCNC NML Config, HowTo. Also note that the RCS_CMD_CHANNEL and RCS_STAT_CHANNEL classes actually derive from the NML base class.

Accessing CMS/NML Buffers

This section will discuss accessing the CMS/NML buffers. From a LinuxCNC user's perspective, the intricate details between CMS and NML interaction are unimportant. Sending/receiving information between NML modules, i.e. the UI (DISPLAY), the LinuxCNC server (EMCSERVER), and the task control module (TASK)3 can be abstractly referred to as CMS/NML buffer operations.

However, for the UI developer, distinction between the CMS buffers and the corresponding NML channels is sometimes necessary. Generally, the UI sends commands over the associated NML channel by (indirectly) invoking one of the class NML methods (e.g. NML::write()). As far as the UI is concerned, this operation initiates information transfer over the underlying IPC channel. On the other hand, obtaining LinuxCNC-core status info is a two-step procedure. First, the UI will (indirectly) invoke, say, the NML::peek() method4. This operation will result in the local instance of the CMS status buffer getting updated with a copy of information from the underlying (NML) IPC channel. The UI will then perform simple pointer dereferencing on the local CMS buffer instance to read the status information.

Sending Commands

The shcom.cc library exports a set of send*() wrapper functions that beautifully hide the details of CMS/NML class methods responsible for handling command transfers. Invoking these wrapper functions will initiate command information transfer across the NML channel via:

emcCommandBuffer->write(&((RCS_CMD_MSG)cmd));

The wrapper functions then poll with a brief timeout to check whether the transfer was successful before returning. If the command transfer was successful, the wrapper functions return 0. Otherwise, in the event of an error, a value of -1 is returned5.

Usage of a few of these wrapper functions is illustrated below:

  • Machine Initialization and Shutdown

    For most machine setups, the sequence of steps involved in preparing the machine for runtime operation, or for shutdown, will resemble:

    • Toggling ESTOP state:

      if (sendEstopReset() == -1) // defined in `shcom.cc`
          // handle error
      ...
      if (sendEstop() == -1) // defined in `shcom.cc`
          // handle error
      

      At startup, the machine is in ESTOP state. On typical physical machine setups, reseting ESTOP takes the machine out of estop state, but the servos still remain disabled. For this, the machine state has to be changed to ON.

    • Toggling ON/OFF machine state:

      if (sendMachineOn() == -1) // defined in `shcom.cc`
          // handle error
      ...
      if (sendMachineOff() == -1) // defined in `shcom.cc`
          // handle error
      
    • Homing axes or rotary joints. A basic homing operation could be:

      // switch to manual mode
      if (sendManual() == -1) // defined in `shcom.cc`
          // handle error
      
      // home all machine axes/joints
      for (i = 0; i < emc_axis_cnt; i++) {
          if (sendHome(i) == -1) {  // defined in `shcom.cc`
              // handle error
          }
          // else, wait till homed (See Section "Monitoring Status Info" below)
      }
      
  • Sending MDI Commands

    • Switching to MDI mode:

      if (sendMdi() == -1) // defined in `shcom.cc` 
          //handle error
      
    • Sending an MDI command

      if (sendMdiCmd(cmdbuf) == -1) // defined in `shcom.cc`
          // handle error
      

      where cmdbuf is a pointer to an MDI command string, e.g. "G0 X0 Y20 Z10".

Monitoring Status Info

The shcom.cc library exports the updateStatus() wrapper function which initiates an NML transfer of status information via:

emcStatusBuffer->peek();

to update the local CMS buffer instance. If an error was encountered, updateStatus() returns -1; otherwise, a return value of 0 indicates a successful CMS/NML peek() operation. A UI may periodically invoke updateStatus() at a rate specified via the CYCLE_TIME parameter in the [DISPLAY] section of the INI file. When the function returns (successfully), the UI can then obtain the updated status info by simple pointer dereferencing on the local CMS buffer instance.

The EMC_STAT Class

Status information in the local CMS buffer instance is organised according to the EMC_STAT class structure (see include/emc_nml.hh). The tryNml() library function (shcom.cc) eventually initializes a emcStatus global pointer to an instance of EMC_STAT soon after instantiating the emcStatusBuffer NML channel for status info:

emcStatus = (EMC_STAT *) emcStatusBuffer->get_address();

where the get_address() method invokes NML::get_address() which, in turn, returns a pointer to the locally allocated CMS buffer instance. The organization of the EMC_STAT class structure,

// include/emc_nml.hh 
class EMC_STAT:public EMC_STAT_MSG {
  public:
    ...
    // the top-level EMC_TASK status class
    EMC_TASK_STAT task;

    // subordinate status classes
    EMC_MOTION_STAT motion;
    EMC_IO_STAT io;
    ...
};

facilitates obtaining status information related to specific LinuxCNC operations: task control, motion, and I/O.

  • EMC_TASK_STAT

    Example usage:

    • Determining machine state and mode:

      if(emcStatus->task.state == EMC_TASK_STATE_ESTOP)
          ...
      if(emcStatus->task.mode == EMC_TASK_MODE_MDI)
          ...
      
    • Obtaining interpreter state:

      The term "interpreter" is used here to refer to the part of the LinuxCNC-core that processes an MDI command. Upon sending an MDI command, the ne_linuxcnc_ui_puma.cc UI suspends the command prompt. It then periodically polls or "peeks into" the CMS/NML status buffer, printing motion status to stdout. When the interpreter completes processing the command, the UI returns the prompt for a new MDI command. This is accomplished via:

      switch (emcStatus->task.interpState) {
      
          case EMC_TASK_INTERP_READING:
          case EMC_TASK_INTERP_WAITING:
          case EMC_TASK_INTERP_PAUSED:
              return 1;  /* interpreter busy */
      
          case EMC_TASK_INTERP_IDLE:
              return 0; /* interpreter idle - move complete */
      
          default:
              return -1; /* an error occurred */
      }
      

      i.e. when the interpreter is in state EMC_TASK_INTERP_IDLE, it implicitly means that the move has completed and there is no more motion status info to display.

      However, it is still possible to queue MDI commands, e.g. by sending one command after another without waiting for the previous one to complete. At least in MDI mode, the EMC_TASK_INTERP_READING implies that the interpreter is still "running", and that the UI may continue to send MDI commands to the interpreter which will, in effect, queue them.

  • EMC_MOTION_STAT

    Example usage:

    • Retrieving MOTMOD status info during homing:

      if (emcStatus->motion.axis[i].homing) {
          printf("AXIS %i:: inpos: %12.4f, outpos: %12.4f, homing...",
               axis,
               emcStatus->motion.axis[i].input,
               emcStatus->motion.axis[i].output);
      }
      
      if (emcStatus->motion.axis[i].homed) {
          printf("AXIS %i:: inpos: %12.4f, outpos: %12.4f, [HOMED]",
               axis,
               emcStatus->motion.axis[i].input,
               emcStatus->motion.axis[i].output);
      }
      

      where i is an index denoting the current axis/joint being homed.

    • Monitoring tool location feedback:

      For example, with a PUMA-type robot manipulator config, the x, y, and z parameters will return tooltip position, while a, b, and c will reflect the the tool's orientation in space.

      if (coords == COORD_MACHINE) {
          if (posDisplay == POS_DISPLAY_ACT) {
              x = emcStatus->motion.traj.actualPosition.tran.x;
              y = emcStatus->motion.traj.actualPosition.tran.y;
              z = emcStatus->motion.traj.actualPosition.tran.z;
              a = emcStatus->motion.traj.actualPosition.a;
              b = emcStatus->motion.traj.actualPosition.b;
              c = emcStatus->motion.traj.actualPosition.c;
          } else {    // POS_DISPLAY_CMD
              x = emcStatus->motion.traj.position.tran.x;
              y = emcStatus->motion.traj.position.tran.y;
              z = emcStatus->motion.traj.position.tran.z;
              a = emcStatus->motion.traj.position.a;
              b = emcStatus->motion.traj.position.b;
              c = emcStatus->motion.traj.position.c;
          }
      }
      
    • Checking axis/joint limits:

      if (emcStatus->motion.axis[i].minHardLimit)
          ...
      if (emcStatus->motion.axis[i].maxHardLimit)
          ...
      if (emcStatus->motion.axis[i].minSoftLimit)
          ...
      if (emcStatus->motion.axis[i].maxSoftLimit)
          ...
      
    • Viewing the number of pending moves including the currently executing MDI command:

      printf("MDI Cmd Queue: %i\n", emcStatus->motion.traj.queue);
      

Intercepting Error Messages

The updateError() wrapper function by shcom.cc initiates an NML transfer for error messages -- if any. If the NML channel read operation encountered problems, the function returns -1; otherwise a value of 0 is returned to indicate success. Note that the return value pertains to errors reading the NML channel, and not to errors reported by the LinuxCNC-core. Otherwise, if an error message was sent by the LinuxCNC-core, then the updateError() function will update the (shcom.cc) global error_string[] buffer (and return 0). The UI can then check for error messages:

#include "shcom.hh" // extern char error_string[NML_ERROR_LEN]
...
memset(error_string, 0, NML_ERROR_LEN);
if (updateError() == -1)
    // handle NML channel read error
if (error_string[0] != '\0')
    // handle LinuxCNC-core error message

LinuxCNC classifies error messages according to type. The updateError() function processes the following error types:

// include/nml_type.hh
#include <stdint.h>
typdef int32_t NMLTYPE;

// include/nml_io.hh
#define NML_ERROR_TYPE    ((NMLTYPE) 1)
#define NML_TEXT_TYPE     ((NMLTYPE) 2)
#define NML_DISPLAY_TYPE  ((NMLTYPE) 3)

// src/emc/emc.hh 
#define EMC_OPERATOR_ERROR_TYPE    ((NMLTYPE) 11) // textual error message to the operator
#define EMC_OPERATOR_TEXT_TYPE     ((NMLTYPE) 12) // textual information message to the operator
#define EMC_OPERATOR_DISPLAY_TYPE  ((NMLTYPE) 13) // URL or filename of a document to display

Testing

The entry LinuxCNC MDI Streaming, Case Study with PUMA 560 presents a few test runs for the ne_linuxcnc_ui_puma.cc program with the LinuxCNC Off-line Programming system.

Also See

Footnotes

1. For an introduction to LinuxCNC modes of operation, see Section 2.7: Modes Of Operation in the LinuxCNC User Manual (available online). In brief, Manual mode allows the operator to jog and home the axes, control the coolant and spindle, etc. Auto mode allows the operator to load NC code programs into the machine, run them, and pause/step/resume them. Manual Data Input (MDI) mode allows the operator to type in single lines, or blocks, of NC code interactively. [go back]

2. Apperently, LinuxCNC will by default use SysV SHM rather than POSIX SHM (libnml/os_intf/_shm.c:rcs_shm_open()). Check out SHM IPC: POSIX SHM vs. Shared File/Anon Mappings for a brief contrast of Linux SHM IPC facilities. [go back]

3. Following the naming conventions for LinuxCNC modules in INI files [go back]

4. Typically, status info is obtained via NML::peek() while error messages are retrieved via NML::read(). Peek operations are the same as reads, except that the CMS buffer header(s) are not updated. See libnml/cms/cms_in.cc. This means that once a read is performed, subsequent reads will return 0 until new data is available for the CMS/NML buffer. On the other hand, peek operations will simply keep returning whatever is currently in the CMS/NML buffer. [go back]

5. This is the general case. One exception where 0 is returned, and yet a send error occurred, is when the NML::write() method fails to find an appropriate CMS write method for the current IPC mechanism in use. At least with the version of LinuxCNC considered here, a 0 is returned by the send*() wrapper function and the CMS error is, instead, printed to stdout. [go back]