Qtvcp is an infrastructure to display a custom CNC screen or control panel in LinuxCNC.
It displays a UI file built with the QTDesigner screen editor or combines this
with python programming to create a GUI screen for running a CNC machine.
Qtvcp is completely customizable - you can add different buttons and status LEDs etc.
or add python code for even finer grain customization.

QTscreen Mill
Figure 1. qtdefault - 3 Axis Sample
QTscreen Qtaxis
Figure 2. Qtaxis - Self Adjusting Axis Sample
QTscreen Blender
Figure 3. Blender - 4 Axis Sample
QTscreen x1mill
Figure 4. X1mill - 4 Axis Sample
QTscreen x1mill
Figure 5. cam_align - Camera alignment VCP

1. Overview

There are two files that can be used, individually or in combination to add
customization.
A UI file that is made with QT’s Designer graphical editor.
A handler file which is a text file with python code.
Normally qtvcp uses the stock UI and handler file.
You can specify qtvcp to use local UI and handler files.
A local file is one that is in the configuration folder that defines the
rest of the machine’s requirements.
One is not restricted to adding a custom panel on the right or a custom tab.
qtvcp leverages QT Designer (the editor) and PyQT5 (the widget toolkit).
QTvcp has some special widgets and actions added just for LinuxCNC.
There are special widgets to bridge third party widgets to HAL pins.
It’s possible to create widget responses by connecting signals to python
code in the handler file.

1.1. QTvcp Widgets

Qtvcp uses the PyQt5 toolkit’s widgets for linuxcnc integration.
Widget is the general name for objects such as buttons and labels in PyQT5.
You are free to use any available widgets in the QTDesigner editor.
There are also special widgets made for linuxcnc that make integration easier.
This are split in three heading on the left side of the editor.
One is for HAL only widgets.
One is for cnc control widgets.
One is for dialog widgets.
you are free to mix them in any way on your panel.
A very important widget for CNC control is the screenoptions widget.
It does not add anything visually to the screen.
But allows important details to be selected rather then be coded in the handler file.

1.2. INI Settings

If you are using this to make a CNC control screen:
Under the [DISPLAY] heading:

DISPLAY = qtvcp <screen_name>
  options:
    -d debugging on
    -a set window always on top
    -c HAL component name. Default is to use the UI file name.
    -g geometry: WIDTHxHEIGHT+XOFFSET+YOFFSET
    -m maximise window
    -f fullscreen the window
    -t theme. Default is system theme
    -x embed into a X11 window that doesn't supoort embedding.
    --push_xid send qtvcp's X11 window id number to standard output; for embedding
    <screen_name> is the base name of the .ui and _handler.py files.
    If <screen_name> is missing the default screen will be loaded.

Qtvcp assumes the UI file and the handler file use this same base name.
Qtvcp will search the LinuxCNC configuration file that was launched first for the files,
then in the system skin folder. the skin folders holds standard screens.

1.3. QTDesigner UI File

A designer file is a text file organized in the XML standard that describes the
layout and the widgets of the screen. Pyqt5 uses this file to build the display
and react to those widgets. The QTDesigner editor makes it relatively easy to build
and edit this file.

1.4. Handler Files

A handler file is a file containing python code, which qtvcp adds to it’s
default routines. A handler file allows one to modify defaults, or add logic
to a qtvcp skin without having to modify qtvcp’s core code.
In this way you can have custom behaviour.
If present a handler file will be loaded.
Only one file is allowed.

1.5. Libraries modules

Qtvcp as built does little more then display the screen and react to widgets.
For more prebuilt behaviours there are available libraries.
(found in lib/python/qtvcp/lib in RIP linuxcnc install)
libraries are prebuilt python modules that give added features to Qtvcp.
In this way you can select what features you want - yet don’t have to build common ones yourself.
Such libraries include:

audio_player
aux_program_loader
keybindings
message
preferences
notify
virtual_keyboard
machine_log

1.6. Themes

Themes are a way to modify the look and feel of the widgets on the screen.
For instance the color or size of buttons and sliders can be changed using themes.
The Windows theme is default for screens. System theme is default for panels.
to see available themes load qtvcp with -d -t SHOWTHEMES

qtvcp can also be customized with Qt stylesheets using css.

1.7. Local Files

If present, local UI files in the configuration folder will be loaded instead
of the stock UI files. Local UI files allow you to use your customized
designs rather then the default screens.
qtvcp will look for MYNAME.ui, MYNAME_handler.py and MYNAME.qss in the launched configuration folder.
You can put these files in a arbitrary named folder - qtvcp will search all folders in your config folder.

1.8. Modifying Stock Screens

If you wish to modify a stock screen, copy it’s UI and handler file to your configuration folder.

2. Build a simple clean-sheet custom screen

QTscreen Mill
Figure 6. Ugly custom screen

2.1. Overview

To build a panel or screen use QTDesigner to build a design you like.
Save this design to your configuration folder with a name of your choice, ending with .ui
modify the configurations INI file to load qtvcp with your new .ui file.
Then connect any required HAL pins in a HAL file

2.2. Get Designer to include linuxcnc widgets

You must have designer installed; These commands should add it:
Or use your package manager to install the same:
sudo apt-get install qttools5-dev-tools
sudo apt-get install qttools5.dev

Then you need the python-module loading library added.
Qtvcp uses QT5 with python2 - this combination is not normally available from
repositories. You can compile it your self or there are precompiled versions
available for common systems.
in lib/python/qtvcp/designer there are folders based on system architectures
and then QT version.
You must pick the cpu architecture folder then pick the series; 5.5, 5.7, or 5.9 of Qt.
currently Debian stretch uses 5.7, Mint 12 uses 5.5, Mint 19 uses 5.9
if in doubt check the version of QT5 on the system.

You must decompress the file and then copy that proper version of
libpyqt5_py2.so to this folder:
/usr/lib/x86_64-linux-gnu/qt5/plugins/designer
(x86_64-linux-gnu might be called something slightly different
on different systems)

You will require super user privileges to copy the file to the folder.

then you must add a link to the qtvcp_plugin.py to the folder that designer will search.

In a RIP version of linuxcnc qtvcp_plugin.py will be in:
~/LINUXCNC_PROJECT_NAME/lib/python/qtvcp/plugins/qtvcp_plugin.py

installed version should be:
usr/lib/python2.7/qtvcp/plugins/qtvcp_plugin.py
or usr/lib/python2.7/dist-packages/qtvcp/plugins/qtvcp_plugin.py

make a link file to the above file and move it to one of the places Designer searches:

Designer searches in these two place for links (pick one):
This can be:
/usr/lib/x86_64-linux-gnu/qt5/plugins/designer/python
or
~/.designer/plugins/python
You may need to add the plugins/python folders

To start Designer:

for a RIP installed:
open a terminal, set the environment for linuxcnc with the command: . scripts/rip-environment
then load designer with : designer -qt=5

otherwise for an installed version, open a terminal and type designer -qt=5

If all goes right you will see the selectable linuxcnc widgets on the left hand side

2.3. build the screen .ui file

When Designer is first started there is a New Form dialog displayed.
Pick Main Window and press the create button.
Do not rename this window - Qtvcp requires the name to be MainWindow

A MainWindow widget is Displayed. Grab the corner of the window and resize to
an appropriate size say 1000x600. right click on the window and click
set minimum size. Do it again and set maximum size.Our sample widget will
now not be resizable.

Drag and drop the screenoption widget onto the main window (anywhere).
This widget doesn’t add anything visually but sets up some common options.

Drag and drop a FileDialog widget on the window anywhere.
This will show the dialog and we want it hidden.
On the right hand side there is a panel with tabs for a Property editor and
an object inspector. On the Object inspector click on the FileDialog. then
switch switch to the property Editor. Under the heading FileDialog toggle
State until the dialog disappears.

Drag and drop a GCodeGraphics widget and a GcodeEditor widget.
Place and resize them as you see fit leaving some room for buttons.

Now we will add action buttons.
Add 7 action buttons on to the main window. If you double click the button, you
can add text. Edit the button labels for Estop, Machine On, Home, Load,
Run, Pause and stop.
Action buttons default to no action so we must change the properties for defined functions.
You can edit the properties directly in the property editor on the right side of designer.
A convenient alternating is right clicking on the button and choosing Set Actions or
Set Indicator Actions. Each will launch a Dialog that allows selecting actions while
only display relevant data to the action.

We will describe the convenient way first:

  • Right click the Machine On button and select Set Actions. When the Dialog displays,
    use the combobox to navigate to MACHINE CONTROLS - Machine On. In this case there there
    is no option for this action so select ok. Now the button will turn the machine on when pressed

And now the direct way with Designer’s property editor

  • Select the Machine On button. Now go to the Property Editor on the right
    side of Designer. Scroll down until you find the ActionButton heading.
    You will see a list of properties and values. find the machine on action and
    click the checkbox. the button will now control machine on/off.

Do the same for all the other button with the addition of:

  • With the Home button we must also change the joint_number property to -1,
    Which tells the controller to home all the axes rather then a specific axis.

  • With the Pause button under the heading Indicated_PushButton check the
    indicator_option and under the QAbstactButton heading check checkable

designer button property
Figure 7. Qt Designer - Selecting Pause button’s properties

We then need to save this design as tester.ui in the sim/qtvcp folder
We are saving it as tester as that is a file name that qtvcp recognizes and
will use a built in handler file to display it.

2.4. Handler file

a handler file is required. It allows customizations to be written in python.
For instance keyboard controls are usually written in the handler file.

In this example the built in file tester_handler.py is automatically used.
It does the minimum required to display the tester.ui defined screen and do
basic keyboard jogging.

2.5. INI

If you are using qtvcp to make a CNC control screen:
Under the [DISPLAY] heading:

DISPLAY = qtvcp <screen_name>

<screen_name> is the base name of the .ui and _handler.py files.

In our example there is already a sim configuration called tester, that we
will use to display our test screen.

2.6. HAL

If your screen used widgets with HAL pins the you must connect then in a HAL file.
qtvcp looks under the heading [HAL] for the entry POSTGUI_HALFILE=<filename>
Typically <filename> would be the screens base name + _postgui + .hal
eg. qtvcp_postgui.hal
These commands are executed after the screen is built, guaranteeing the widget HAL
pins are available.

In our example there are no HAl pins to connect.

3. Handler file in detail

handler files are used to create custom controls using python.

3.1. Overview

Here is a sample handler file.
It’s broken up in sections for ease of discussion.

############################
# **** IMPORT SECTION **** #
############################
import sys
import os
import linuxcnc

from PyQt5 import QtCore, QtWidgets

from qtvcp.widgets.mdi_line import MDILine as MDI_WIDGET
from qtvcp.widgets.gcode_editor import GcodeEditor as GCODE
from qtvcp.lib.keybindings import Keylookup
from qtvcp.core import Status, Action

# Set up logging
from qtvcp import logger
LOG = logger.getLogger(__name__)

# Set the log level for this module
#LOG.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL

###########################################
# **** INSTANTIATE LIBRARIES SECTION **** #
###########################################

KEYBIND = Keylookup()
STATUS = Status()
ACTION = Action()
###################################
# **** HANDLER CLASS SECTION **** #
###################################

class HandlerClass:

    ########################
    # **** INITIALIZE **** #
    ########################
    # widgets allows access to  widgets from the qtvcp files
    # at this point the widgets and hal pins are not instantiated
    def __init__(self, halcomp,widgets,paths):
        self.hal = halcomp
        self.w = widgets
        self.PATHS = paths

    ##########################################
    # SPECIAL FUNCTIONS SECTION              #
    ##########################################

    # at this point:
    # the widgets are instantiated.
    # the HAL pins are built but HAL is not set ready
    # This is where you make HAL pins or initialize state of widgets etc
    def initialized__(self):
        pass

    def processed_key_event__(self,receiver,event,is_pressed,key,code,shift,cntrl):
        # when typing in MDI, we don't want keybinding to call functions
        # so we catch and process the events directly.
        # We do want ESC, F1 and F2 to call keybinding functions though
        if code not in(QtCore.Qt.Key_Escape,QtCore.Qt.Key_F1 ,QtCore.Qt.Key_F2,
                    QtCore.Qt.Key_F3,QtCore.Qt.Key_F5,QtCore.Qt.Key_F5):

            # search for the top widget of whatever widget received the event
            # then check if it's one we want the keypress events to go to
            flag = False
            receiver2 = receiver
            while receiver2 is not None and not flag:
                if isinstance(receiver2, QtWidgets.QDialog):
                    flag = True
                    break
                if isinstance(receiver2, MDI_WIDGET):
                    flag = True
                    break
                if isinstance(receiver2, GCODE):
                    flag = True
                    break
                receiver2 = receiver2.parent()

            if flag:
                if isinstance(receiver2, GCODE):
                    # if in manual do our keybindings - otherwise
                    # send events to gcode widget
                    if STATUS.is_man_mode() == False:
                        if is_pressed:
                            receiver.keyPressEvent(event)
                            event.accept()
                        return True
                elif is_pressed:
                    receiver.keyPressEvent(event)
                    event.accept()
                    return True
                else:
                    event.accept()
                    return True

        # ok if we got here then try keybindings
        try:
            return KEYBIND.call(self,event,is_pressed,shift,cntrl)
        except NameError as e:
            LOG.debug('Exception in KEYBINDING: {}'.format (e))
        except Exception as e:
            LOG.debug('Exception in KEYBINDING:', exc_info=e)
            print 'Error in, or no function for: %s in handler file for-%s'%(KEYBIND.convert(event),key)
            return False

    ########################
    # CALLBACKS FROM STATUS #
    ########################

    #######################
    # CALLBACKS FROM FORM #
    #######################

    #####################
    # GENERAL FUNCTIONS #
    #####################

    # keyboard jogging from key binding calls
    # double the rate if fast is true
    def kb_jog(self, state, joint, direction, fast = False, linear = True):
        if not STATUS.is_man_mode() or not STATUS.machine_is_on():
            return
        if linear:
            distance = STATUS.get_jog_increment()
            rate = STATUS.get_jograte()/60
        else:
            distance = STATUS.get_jog_increment_angular()
            rate = STATUS.get_jograte_angular()/60
        if state:
            if fast:
                rate = rate * 2
            ACTION.JOG(joint, direction, rate, distance)
        else:
            ACTION.JOG(joint, 0, 0, 0)

    #####################
    # KEY BINDING CALLS #
    #####################

    # Machine control
    def on_keycall_ESTOP(self,event,state,shift,cntrl):
        if state:
            ACTION.SET_ESTOP_STATE(STATUS.estop_is_clear())
    def on_keycall_POWER(self,event,state,shift,cntrl):
        if state:
            ACTION.SET_MACHINE_STATE(not STATUS.machine_is_on())
    def on_keycall_HOME(self,event,state,shift,cntrl):
        if state:
            if STATUS.is_all_homed():
                ACTION.SET_MACHINE_UNHOMED(-1)
            else:
                ACTION.SET_MACHINE_HOMING(-1)
    def on_keycall_ABORT(self,event,state,shift,cntrl):
        if state:
            if STATUS.stat.interp_state == linuxcnc.INTERP_IDLE:
                self.w.close()
            else:
                self.cmnd.abort()

    # Linear Jogging
    def on_keycall_XPOS(self,event,state,shift,cntrl):
        self.kb_jog(state, 0, 1, shift)

    def on_keycall_XNEG(self,event,state,shift,cntrl):
        self.kb_jog(state, 0, -1, shift)

    def on_keycall_YPOS(self,event,state,shift,cntrl):
        self.kb_jog(state, 1, 1, shift)

    def on_keycall_YNEG(self,event,state,shift,cntrl):
        self.kb_jog(state, 1, -1, shift)

    def on_keycall_ZPOS(self,event,state,shift,cntrl):
        self.kb_jog(state, 2, 1, shift)

    def on_keycall_ZNEG(self,event,state,shift,cntrl):
        self.kb_jog(state, 2, -1, shift)

    def on_keycall_APOS(self,event,state,shift,cntrl):
        pass
        #self.kb_jog(state, 3, 1, shift, False)

    def on_keycall_ANEG(self,event,state,shift,cntrl):
        pass
        #self.kb_jog(state, 3, -1, shift, linear=False)

    ###########################
    # **** closing event **** #
    ###########################

    ##############################
    # required class boiler code #
    ##############################

    def __getitem__(self, item):
        return getattr(self, item)
    def __setitem__(self, item, value):
        return setattr(self, item, value)

################################
# required handler boiler code #
################################

def get_handlers(halcomp,widgets,paths):
     return [HandlerClass(halcomp,widgets,paths)]

3.2. IMPORT SECTION

This section is for importing library modules required for your screen.
It would be typical to import qtvcp’s keybinding, Status and action
libraries.

3.3. INSTANTIATE LIBRARIES SECTION

By instantiating the libraries here we create global reference.
You can note this by the commands that don’t have self. in front of them.
By convention we capitalize the names of global referenced libraries.

3.4. HANDLER CLASS section

The custom code is placed in a class so qtvcp can utilize it.
This is the definitions on the handler class.

3.5. INITIALIZE section

Like all python libraies the init function is called when the library
is first instantiated. You can set defaults and reference variables here.
The widget references are not available at this point.
The variables halcomp, widgets and paths give access to qtvcp’s HAL component,
widgets, and path info respectably.
This is where you would set up global variables.
Widgets are not actually accessible at this point.

3.6. SPECIAL FUNCTIONS section

There are several special functions that qtvcp looks for in the handler file.
If qtvcp finds these it will call them, if not it will silently ignore them.

3.6.1. initialized__(self):

This function is called after the widgets and HAL pins are built
You can manipulate the widgets and HAL pins or add more HAL pins here.
Typically preferences can be checked and set, styles applied to
widgets or status of linuxcnc be connected to functions.
This is also where keybindings would be added.

3.6.2. class_patch__(self):

Class patching allow you to override function calls in an imported module.
Class patching must be done before the module is instantiated and it modifies
all instances made after that.
An example might be patching button calls from the gcode editor to call functions
in the handler file instead.

3.6.3. processed_key_event__(self, receiver,event,is_pressed,key,code,shift,cntrl):

This function is called to facilitate keyboard jogging etc.
By using the keybindings library this can be used to easily add
functions bound to keypresses.

3.6.4. keypress_event__(self,receiver, event)):

This function gives raw key press events. It takes presidence over
the processed_key_event.

3.6.5. keyrelease_event__(receiver, event):

This function gives raw key release events. It takes presidence over
the processed_key_event.

3.6.6. before_loop__(self):

This function is called just before the Qt event loop is entered.
At the point all widgets/libraries/initialization code has completed and the screen is already displayed.

3.6.7. system_shutdown_request__(self):

If present, this function overrides the normal function called when a user selects a total system shutdown.
It could be used to do pre-shutdown housekeeping. The system will not shutdown if using this function, you will
have to do that yourself. qtvcp/linuxcnc will shutdown wihtout a prompt after this function returns

3.6.8. closing_cleanup__(self):

This function is called just before the screen closes. It can be used
to do cleanup before closing.

3.7. STATUS CALLBACKS section

By convention this is where you would put functions that are callbacks
from STATUS definitions.

3.8. CALLBACKS FROM FORM section

By convention this is where you would put functions that are callbacks
from the widgets that you have connected to the MainWindow with the
designer editor.

3.9. GENERAL FUNCTIONS section

By convention this is where you put your general functions

3.10. KEY BINDING section

If you are using the keybinding library this is where you place your
custom key call routines.
The function signature is:

    def on_keycall_KEY(self,event,state,shift,cntrl):
        if state:
            self.do_something_function()

KEY being the code (from the keybindings library) for the desired key.

3.11. CLOSING EVENT section

Putting the close event function here will catch closing events.
This replaces any predefined closeEvent function from qtvcp
It’s usally better to use the special closing_cleanup__ function.

    def closeEvent(self, event):
        self.do_something()
        event.accept()

4. Connecting widgets to python code

It’s possible to connect widgets to python code using signals and slots.
In this way you can give new functions to linuxcnc widgets or utilize
standard widgets to control linuxcnc.

4.1. Overview

In the Designer editor you would create user function slots and connect
them to widgets using signals.
In the handler file you would create the slot’s functions defined in Designer.

4.2. Using Designer to add slots

When you have loaded your screen into designer add a plain PushButton to the screen.
You could change the name of the button to something interesting like test_button
There are two ways to edit connections - This is the graphical way
There is a button in the top tool bar of designer for editing signals.
After pushing it, if you click-and-hold on the button it will show a arrow
(looks like a ground signal from electrical schematic)
Slide this arrow to a part of the main window that does not have widgets on it.
A Configure Connections dialog will pop up.
The list on the left are the available signals from the widget.
The list on the right is the available slots on the main window and you can add to it.

Pick the signal clicked() - this makes the slots side available.
click edit on the slots list.
A Slots/Signals of MainWindow dialog will pop up.
On the slots list at the top there is a plus icon - click it.
you can now edit a new slot name.
Erase the default name slot() and change it to test_button()
press the ok button.
You’ll be back to the Configure Connections dialog.
now you can select your new slot in the slot list.
then press ok and save the file.

QTvcp
Figure 8. Designer signal/slot selection

4.3. Handler file changes

Now you must add the function to the handler file.
The function signature is def slotname(self):
We will add some code to print the widget name.

So for our example:

def test_button(self):
    name = self.w.sender().text()
    print name

Add this code under the section named:

#######################
# callbacks from form #
#######################

In fact it doesn’t matter where in the handler class you put the commands but by convention this is where to put it.
Save the handler file.
Now when you load your screen and press the button it should print the name
of the button in the terminal.