Modbus communication with HuanYang VFD and PlanetCNC TNG – Part 2

If connection test was successful, and you can see parameter values displayed in the output window, then we can continue with next step, which is controlling the spindle.

In settings under File/Settings/User Interface/Commands/Spindle Command -> enable option Uses M-Code:

Type M3/M4 into the MDI window and hit Enter. Inverter should start the spindle at set spindle speed, and LED display should show RPM value(if display is in RPM mode).

Output window should print the following text (debug set to 1):

Type M5 into the MDI window and hit Enter. Inverter should stop the spindle:

Expr_VFD_beta.txt file includes also error checking code, which will generate error log file that will log all error events during the communication between the TNG and inverter.
These inverters tend to respond slowly or faulty to external commands and additional retry is necessary. This log file will record how many of such retries there were during the communication.

Example of Error log file and its content:

Two error events, for which in both cases occurred during the Reverse speed set command:

 

 

Troubleshooting

If inverter will not respond to any spindle gcode command , it indicates that something is not ok.
Type into the MDI window the following text:

=debug = 1

Test the M3, M4 and M5 codes again.

Observe the output window and the printed text.

 

 

 

Modbus communication with HuanYang VFD and PlanetCNC TNG – Part 1

This tutorial will describe the necessary steps for successful control of your HuanYang VFD(model: HY01D523B) using PlanetCNC TNG software.

Before we start, I would really recommend reading tutorials related to serial communication, MODBUS and PlanetCNC TNG software. These tutorials explain the tools provided by PlanetCNC and how to use them in order to communicate with external equipment using serial communication(MODBUS).

Description of serial communication functions(data preparation):
Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 1

Description of serial communication functions(data send/read):
Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 2

Establishing communication with Modbus device(basic):
Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 3

Establishing communication with Modbus device:(advanced)
Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 4

 

1. Introduction

HuanYang inverters are very common and popular among hobby CNC machinists. They can be used for different applications, but in a CNC machine context they are normally used to drive the spindle motor of CNC machine.

Through parameter configuration, user can configure inverter behavior in such way, so that it complies with application needs and spindle motor hardware requirements that it is used with.
Such CNC application requirement would be achieving spindle control trough gcode program commands. Meaning, whenever commands such as M3, M4, M5 appear in the gcode program, spindle motor of CNC machine should respond with appropriate action.
Example: M3 S10000 should turn ON the spindle and ramp up spindle to 10000 rpm, M4 should rotate spindle motor in reverse direction and M5 gcode command should stop the spindle.

There are multiple ways to achieve this. User can manually operate inverter from front panel, and press the buttons and rotate the knobs at the right moment – not really useful.

Second would be that inverter control terminal inputs will control the motor behavior. In practice this means that external board will be needed for inverter input control.
Example: User connects relay board with cnc controller and configure cnc software in such way, that controller output pins will respond to gcode commands and activate appropriate  relays. Stable spindle speed control can be difficult to achieve due to analog voltage signals and  EM interference.

Last and the most elegant way would be to control the inverter using MODBUS communication. No relays, no additional wires, no analog voltage etc..
Example: PlanetCNC software sends appropriate data trough COM port of computer via MODBUS protocol to inverter. MODBUS is by definition used with RS-485 electrical interface, which is very robust and resistant to EM interference.

 

This part of tutorial will describe how to:

-connect HuanYang inverter with PC using a USB to RS-485 adapter
-key Modbus related inverter parameter configuration
-Expression file manipulation
-Communication test

 

2. Hardware used

HuanYang inverter:
HuanYang VFD model: HY01D523B

USB to RS-485 converter:
My PC does not have COM port, let alone a RS-485 interface. This is why I will use USB to RS-485 converter. Such device is classified as a USB CDC(communication device class) device and emulates COM port of my computer.

It makes sense to buy or own two pieces of such adapter converter. Many times these cheap units malfunction or do not even work from the start. So I recommend that you make a test with two of these adapters and create a loop, where you can test their functionality.

Such test is described to some extent in Modbus relay board tutorial:
Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 2

PlanetCNC TNG software:
As a master device, we will use PlanetCNC TNG software, which will send and receive data trough a PC’s COM port.
NOTE: Please use latest available version from here: https://planet-cnc.com/software/

 

3. Inverter parameter configuration

NOTE: Make sure that main power supply and spindle motor are connected as per user manual of inverter. Connection of power supply and spindle motor as also spindle motor related parameters are not subject of this tutorial.

Once inverter is turned ON and you can access the parameter menu, you will need to change some parameters. The following parameters refer to MODBUS communication:

PD001-> set to value 2
Source of run commands: Communication port

PD002-> set to value 2
Source of operating frequency: Communication port

PD163-> set to value 1
Communication address: Device address set to 1

PD164-> set to value 1
Communication Baud Rate: 9600 b/s

PD165 -> set to value 3
Communication data method: 8N1 for RTU. 8 data bits, no parity bit and 1 stop bit.

Use front panel buttons to navigate trough the parameter menu.

 

4. Connection of USB to RS-485 adapter and inverter

Connect A(D+) terminal of RS-485 adapter with RS+ terminal of inverter, using first wire of the twisted pair.
Connect B(D-) terminal of RS-485 adapter with RS- terminal of inverter, using second wire of the twisted pair.
Connect adapter in one of the PC’s USB slots.

 

Under Device Manager check COM ports ID. In my case, adapter is recognized as  “COM10”:

This will be important piece of information, when we will edit the Expression file which is responsible for serial communication.

 

5. Expression and Gcode script files

For proper functionality user needs to place additional files to its TNG profile folder.
Four expression files should be placed in the root profile folder, and three gcode script files should be placed in Scripts folder.

To download file(s), click right mouse button and select Save Link as…

Expression files:

Expr_VFD_beta.txt -> download

Expr_Modbus_HuanYang_VFD_control_speed.txt -> download

Expr_Modbus_HuanYang_VFD_control.txt -> download

Expr_Modbus_HuanYang_VFD_Parameter_Read.txt -> download

Gcode script files: 

M3.gcode -> download

M4.gcode -> download

M5.gcode -> download

 

File locations: 

 

Expr_VFD_beta.txt                           -> file should be placed/pasted in the root folder of user profile folder
Expr_Modbus_HuanYang_VFD_Parameter_Read.txt -> file should be placed/pasted in the root folder of user profile folder
Expr_Modbus_HuangYang_VFD_control_speed.txt -> file should be placed/pasted in the root folder of user profile folder
Expr_Modbus_HuangYang_VFD_control.txt       -> file should be placed/pasted in the root folder of user profile folder

M3.gcode -> file should be placed/pasted in the Scripts folder
M4.gcode -> file should be placed/pasted in the Scripts folder
M5.gcode -> file should be placed/pasted in the Scripts folder

6. Expression file configuration

Open Expr_Modbus_HuanYang_VFD_Parameter_Read.txt file in your text editor, and at #OnInit section of the file check if variables below, correspond with parameters of inverter.

port = "COM10"  -> should match to the ID of your USB to RS-485 adapter under Device Manager/Ports(COM & LPT)
baudrate = 9600 -> should correspond to the value of inverter parameter PD164 ; in my case, inverter parameter value PD164 is set to 1 (baudrate 9600)
bits = 8        -> should correspond to the inverter parameter PD165 ; in my case, inverter parameter value PD165 is set to 3 (8N1 for RTU, 8 data bits, no parity bit, 1 stop bit)
parity = 0      -> should correspond to the inverter parameter PD165 ; in my case, inverter parameter value PD165 is set to 3 (8N1 for RTU, 8 data bits, no parity bit, 1 stop bit)
stopbits = 1    -> should correspond to the inverter parameter PD165 ; in my case, inverter parameter value PD165 is set to 3 (8N1 for RTU, 8 data bits, no parity bit, 1 stop bit)
addr = 1        -> should correspond to the inverter parameter PD163 ; in my case, inverter parameter PD163 value is set to 1 (device address 1)

Same should be done with Expr_Modbus_HuangYang_VFD_control.txt file.

 

7. Communication test

Open TNG software, and under View/Panel make sure that Utilities option is enabled.

Output window should be displayed at the bottom middle section:

-Check if USB to RS-485 adapter is connected with PC
-Check if USB to RS-485 adapter is recognized under Control Panel/Device Manager
-Check if connection with twisted pair between inverter and adapter is correct
-Check if inverter is turned ON
-Check if Modbus related inverter parameters are correct
-Check if Expression and gcode file locations are correct
-Check if COM name of adapter matches the name variable in Expression files under #OnInit section.
-Check if serial communication parameter values in Expression files under #OnInit section matches inverter parameter values

 

In the MDI window type following text:

=exec('#Modbus_HuanYang_VFD_Parameter_read')

This will start an expression function which will print inverter communication and  motor related parameters into the output window as also create a .txt file in main user profile. So, after hitting Enter, with any luck, you should se this in the output window:

As also newly created file in your root profile folder:

8. Troubleshooting

If it takes long period of time for procedure to finish and nothing is printed into the output window, it indicates that something is not ok.
Type into the MDI window the following text:

=debug = 1

Now run the procedure again:

=exec('#Modbus_HuanYang_VFD_Parameter_read')

Observe the output window and the printed text.
If any COM port related problem exists, such as adapter is not connected, or COM name does not match the one in #OnInit section of expression files, this will be displayed :

If the response from script will display “response check failed with error code: -1”, this means that the returned data from inverter is not as per specification of this modbus expression function.

 

In such case, it makes sense to investigate more in detail, and see what exactly is returned from the inverter.

In the file Expr_Modbus_HuanYang_VFD_Parameter_Read.txt, edit line 109, and uncomment it(remove the ; symbol):

Save file, and restart TNG, or just open settings and confirm them. This will reload the modified expression file.

When the procedure is executed again, returned data will also be printed:

If printed values do not make sense or there is no printed data, we can dig deeper and search for a reason of this issues somewhere else, maybe faulty adapter or inverter is not configured correctly/has unresponsive terminal.

 

Next part will describe spindle control: Part 2

 

 

 

Laser cutting New Year’s decoration with PlanetCNC TNG

Holiday season is upon us, and with global warming being more and more aggressive, we cannot rely on much snow during the holidays. This leaves us with no other choice than to make snowflakes ourselves – using paper and CNC laser cutter.

Luckily, PlanetCNC TNG software program feature Image Import  comes in great help, so scissors will not be necessery. Image Import offers all necessary tools to import and process an image file in order to generate suitable gcode for our CNC machining application (laser cutting).

Obtain image

In your web browser search for “Snowflake silhouette”. Choose the one that you like the most.

Click on the image with your right mouse button and select “Copy Image”:

Open PlanetCNC TNG software, and in the gcode window paste this image with right mouse button using “Paste From Clipboard”:

Image import dialog window will open:

Image import configuration

 

Generate:

This group of settings offers tools on how the image toolpath will be generated. Since we only want our snowflake to be cutout from paper in a 2D fashion, we select the Outline option.

Image size: 

Original image size in pixels is automatically recognized. User can change image size with Size XY input fields. Note that aspect ratio will be preserved.
I will set new size based on my laser cutter’s workspace.

Edge Detection:

Outline considers only the silhouette of the image, in other words outline of the shape.

Quantize and Single Channel:

Quantize value of 2 means that only two colors will be considered in image processing. This gives us the best possible contrast for outline edge dectection.

 

Preview button will toggle between the original image and generated toolpath as per current setting configuration:

 

When we are satisfied with the toolpath, OK button brings us to further import dialogue, where we will configure cutting parameters:

Mind that we will be using laser, so Height parameters are not considered (if laser cutter does not use Z axis).

Outputs: 

Speed:  This value represents S value when spindle (Laser) will be turned ON.

Top – Off (Spindle): Laser will be turned OFF for all traverse moves

Bottom – On (Laser): Laser will be turned ON for all feed moves

It is worth mentioning that suitable values are used for Spindle speed as per one’s machine settings.

Click OK, and software will generate gcode toolpath:

Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 4

Last part of this tutorial series will describe how to properly design the expression code for Modbus serial communication.

In previous part we made two expression functions. One was responsible for turning relay ON the other one for turning it OFF. Our main concern was just to make it work, to see that relay clicks and that was it. Now we will try to do it the right way, using an efficient, structured and optimized code design. And to add to the complexity, we will use relay board  as flood actuator, so that M8/M9 gcode command activates/deactivates relay 1 of  Modbus relay board.

Create dedicated expression file

In root folder of profile folder create new text file. Use descriptive name for better  file transparency and organization. I used Expr_Modbus_RelayBoard_8.txt.
Such file name gives all information: This is an expression file, related to Modbus communication and dedicated to the relay board:

sdfbg

 

#OnInit and #OnShutdown functions

This is a built in event function which is executed only once, that is when PlanetCNC TNG software is initialized and/or when settings configuration is confirmed. We can associate this as a machine startup event. This is an ideal timing for initialization of certain parameters and/or variables. Initialization sets initial conditions and therefore predicted behavior.

To put this in context, lets take a look at the serial_open() function. It is used to open and configure COM port, and it accepts six parameters:

serial_open(port, baudrate, bits, parity, stopbits, flowcontrol)

Implementation: 
serial_open("COM10", 9600, 8, 0, 1, 0);

If function is used on multiple locations in the program, tracking its parameter configuration can be time consuming and prone to mistakes.

One way of avoiding this is to use variables as function parameters, which  are created and initialized within the #OnInit function.  This is very convenient when COM port configuration is changed due to different device or hardware settings etc.  With such approach we only change the variable values once while function implementation is left unchanged.

#OnInit
port = "COM10";
baudrate = 9600;
bits = 8;
parity = 0;
stopbits = 1;
flowcontrol = 0;
_relay_flood = 4;
Implementation: 
serial_open(port, baudrate, bits, parity, stopbits, flowcontrol)

NOTE: For this function we used local variables(port, baudrate, bits ect…) and global variable(_relay_flood).  Local variables scope is limited to only this Expr_Modbus_RelayBoard_8.txt file. Local variables do not have any effect and cannot be in conflict with variables using same name outside of this file. Global variable’s scope is not limited to local expr file, but its value and modification can be used in global files of TNG software, these can be other expr files or gcode program files or script files.

#OnShutdown function is also a one time event, this is when PlanetCNC TNG software is closed. We can associate this as a machine shutdown event.

 

Functions for Modbus data preparation, sending/reading and verifying

Instead of creating a function which would perform data preparation, sending/reading and verification all together, we can create three separate functions, each having its own purpose. This gives our Expr_Modbus_Relay_8.txt file a much more structured and transparent look.

  • Function  for data preparation
  • Function for send/read of request/response data
  • Function for response data verification

 

Function for data preparation

M8 script code will be modified in such way so that desired relay will be activated once flood state is activated.

M8 script code uses (expr, ) gcode comment command, which executes any expression function available within TNG software.

(expr, exec())

exec() expression function accepts multiple arguments. Argument can be expression function, value, parameter etc.. All are executed in sequential order, from first to last. Once the exec() function is executed, its arguments can be passed to other functions. And this is exactly how it is used here.

First argument is #<_relay_flood>:  This is a global parameter that is created by the user, and it defines the relay number of modbus relay board. This parameter is defined within the #OnInit function of our Expr_Modbus_Relay_8.txt.  See chapter above for use of implementation. This argument will be passed to function #Modbus_Relay as argument 1.

Second argument is 1: This is the value of relay state, 1 means that we want to turn relay ON. This argument will be passed to function #Modbus_Relay as argument 2.

Third argument is “#Modbus_Relay”: This is an expression function call. Function #Modbus_Relay is located in Expr_Modbus_RelayBoard_8.txt file.

M8 gcode command finally sets the M8 gcode modal state.

 

Whole M8 script code is below:

(expr, exec(#<_relay_flood>, 1, "#Modbus_Relay")) 
M8

Useful aspect of such approach is that we can use it with multiple script files. Only difference is that we change the relay number and state value.

 

#Modbus_Relay function will be solely responsible for data preparation that is sent to the relay board.

In order that relay 1 will either turn ON/OFF, correctly prepared and  structured data needs to be sent to the relay board. Modbus ADU includes device address ID byte, function code byte, data bytes and CRC bytes.

Device address and function code are defined with private variables: .addr and .fc:

.addr = 1; 
.fc = 0x05;

Relay number and its state(ON/OFF) are defined with private variables .relay_number, .relay_state. As mentioned earlier, their values are passed as argument values of exec function executed from M8 script file:

.relay_number = .arg1;
.relay_state = .arg2;

As per boards user manual, device response data length is 8 bytes. This value is defined with variable .resp_size. This variable is important for response data verification.

.resp_size = 8;

5th byte of the ADU accepts 0x00 for relay OFF or 0xFF for relay ON. Since .relay_state’s value is a passed as an argument value where we only use values 0 or 1, line below converts 0 to 0x00 or 1 to 0xFF:

if(.relay_state, .relay_state = 0xFF, .relay_state = 0x00);

New array handle is created where the ADU data is set:

.payload = array_new();

Array data consists of device address ID byte, function code byte, zero byte, relay number byte, relay state byte and zero byte. CRC bytes will be appended in the #Modbus_Write_ReadData function before sending the data.

array_setdata(.payload, 0, .addr, .fc, 0x00, .relay_number-1, .relay_state, 0x00);

Now that our request data (.payload array) is more or less set, it will be same as before, passed as argument 1 to the #Modbus_Write_ReadData function.

.resp_size will be passed as argument 2.

.rc = exec(.payload, .resp_size, '#Modbus_Write_ReadData');

 

Whole #Modbus_relay code is below:

#Modbus_Relay
debug('BEGIN #Modbus_Relay');

.relay_number = .arg1;
.relay_state = .arg2;

.addr = 1;
.fc = 0x05;
.resp_size = 8;

if(.relay_state, .relay_state = 0xFF, .relay_state = 0x00);
.payload = array_new();
array_setdata(.payload, 0, .addr, .fc, 0x00, .relay_number-1, .relay_state, 0x00);
.rc = exec(.payload, .resp_size, '#Modbus_Write_ReadData');
if(.rc != 0, exec(array_delete(.payload), return(-1)));
array_delete(.payload);
debug('END #Modbus_Relay');

NOTE: For this function we used private variables(.relay_number, .relay_state, .addr ect…) and global variable(_relay_flood).  Private variables visibility scope is limited by the expression function where it is used. Private variables do not have any effect and cannot be in conflict with variables using same name outside of expression function where they are used.

 

Function for send/read of request/response data

This function will be responsible for sending the request data and reading the response data over serial port.

Request data and size value are passed as argumens 1 and 2 from #Modbus_Relay function:

.payload = .arg1;
.resp_size = .arg2;

COM port is initialized and opened:

.rc = serial_open(port, baudrate, bits, parity, stopbits, flowcontrol);

Address ID and function code bytes are saved as private variables .addr and .fc. These variables are important for response data verification:

.addr = array_getdata(.payload, 0); 
.fc = array_getdata(.payload, 1);

Response data is set to .payload array and passed as an argument together with .addr, .fc and .resp_size variables to the #Check function.

serial_readarray(port, .payload, 150);
.rc = exec(.payload, .addr, .fc, .resp_size, '#Check');

Whole #Modbus_Write_ReadData code is below:

#Modbus_Write_ReadData

debug(str(" ",2),'BEGIN #Modbus_Write_ReadData');

.payload = .arg1;
.resp_size = .arg2;

;COM port init
.rc = serial_open(port, baudrate, bits, parity, stopbits, flowcontrol);
if(.rc != 0, exec(debug('COM port not available'), return(-1)));

.addr = array_getdata(.payload, 0);
.fc = array_getdata(.payload, 1);

;crc16 calculation 
.crc = array_crc16(.payload, 0, -1);

;set array data with crc
array_setdata16(.payload, -1, .crc);

;send array data 
serial_writearray(port, .payload);

array_clear(.payload);

;--------------------------------------------------------------------------
;read array data 
serial_readarray(port, .payload, 150);

;check returned array data
.rc = exec(.payload, .addr, .fc, .resp_size, '#Check');
if (.rc, exec(debug('response check failed with error code: ', .rc), serial_close(port), return(-1)));

serial_close(port);
debug(str(" ",2),'END #Modbus_Write_ReadData');

return(0);

 

Function for response data verification

This function is responsible for response data verification. Verification process basically compares values of address ID, function code, response size and CRC numbers of request and response data.

Response data, address ID(expected), function code(expected) and size(expected) are passed as arguments from #Modbus_Write_ReadData function and saved as private variables:

.hnd = .arg1;
.addr = .arg2;
.fc = .arg3;
.size = .arg4;

And then compared with actual response address ID, function code and size data:

if (.size != array_size(.hnd), return(-1));

if(.addr != array_getdata(.hnd, 0), return(-3));

if(.fc != array_getdata(.hnd, 1), return(-4));

Response CRC value is calculated from response data (without last two CRC bytes) and compared with response CRC bytes:

.crc1 = array_getdata16(.hnd, .size - 2);

.crc2 = array_crc16(.hnd, 0, .size - 2);

 if(.crc1 != .crc2, return(-2)); debug(str(" ",4),'CRC OK: ', .crc2); 

Whole #Check code is below:

#Check
debug(str(" ",3),'BEGIN #Check');

.hnd = .arg1;
.addr = .arg2; 
.fc = .arg3; 
.size = .arg4;

;compare size with array size
if (.size != array_size(.hnd), return(-1));

;read crc1 from data 
.crc1 = array_getdata16(.hnd, .size - 2);

;calc crc2 from data
.crc2 = array_crc16(.hnd, 0, .size - 2);

if(.crc1 != .crc2, return(-2));
debug(str(" ",4),'CRC OK: ', .crc2);

;compare addr with data from array
if(.addr != array_getdata(.hnd, 0), return(-3));
debug(str(" ",4),'Address OK: ', .addr);

;compare fc with data from array 
if(.fc != array_getdata(.hnd, 1), return(-4));
debug(str(" ",4),'Function Code OK: ', .fc);

debug(str(" ",3),'END #Check');

return(0);

Beautiful thing about such code design is that if we need to modify script code for M9 and M18 gcode files(to turn the relay OFF), we only do this:

;M18 code:
o<chk> if[NOT[EXISTS[#<pvalue>]]]
#<pvalue> = 0
o<chk> endif

(expr, exec(#<_relay_flood>, #<pvalue>, "#Modbus_Relay"))

M18 P#<pvalue>
;M9 code:
(expr, exec(#<_relay_flood>, 0, "#Modbus_Relay"))
M9

Functions #Modbus_Relay, #Modbus_Write_ReadData and #check will do the rest.

 

Below is visual representation of argument passing between files and/or functions:

 

 

 

 

Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 3

In this part, we will use and configure Expr.txt file so that we will be able to control relay board via serial communication using Modbus protocol. Expression array and serial functions described in first two parts will be used in order to achieve this.

 

Modbus supports communication to and from multiple devices connected to the same wire of serial communication line.

In this tutorial we will use MODBUS RTU frame format.

Modbus “frame” consists of an Application Data Unit (ADU), which encapsulates a Protocol Data Unit (PDU):

ADU = Address + PDU + Error check

PDU = Function code + Data

If we put the table above in context, with TNG we will send suitable ADU frame to our slave device(relay board) in order that we will successfully control it.

By using array and serial expression functions, we will manipulate data in such way, that each byte will be at correct value and position of transmitted data.

We will use boards user manual for reference regarding the suitable data.

For more about the Modbus protocol you can read on its official site:
https://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf

 

Hardware used

As mentioned earlier, we will use Modbus RTU frame format, which by definition uses RS-485 electrical interface – differential signalling over twisted pair wire.

Relay board uses RS-485 communication interface and by that we need to make sure that differential line is terminated using termination resistor. In our case this is done by placing a jumper in proper position(on board). If multiple boards are used in daisy chain fashion, only last board needs to terminate the differential line. By doing this we prevent signal reflection and therefore data corruption.
This board will be our Modbus slave device.

As a master device, we will use PlanetCNC TNG software, which will send and receive data trough a PC’s COM port. My PC does not have COM port, let alone a RS-485 interface. This is why I will use USB to RS-485 converter. Such device is classified as a USB CDC(communication device class) device and emulates COM port of my computer.

Under Device Manager we can check COM ports ID as “COM11”:

RS-485 converter and relay board are connected using twisted pair wire.

 

Identifying relay board

Since this board uses Modbus protocol on top of serial communication(UART), we need to obtain  board’s serial communication parameters.

Namely, we need boards address ID and baudrate value.  Address ID is in domain of Modbus and it is an important piece of information, since it defines which slave device will respond to our transmitted data. In most cases, Modbus devices use DIP switches for address ID configuration or have some other means for configuring. This relay board does not use DIP switches and we will need to identify the device with different approach.

For start, we need some information about COM port that we intend to use. Specifically,  COM ports name, which is important parameter of the serial_open() command..

Type into the MDI:

=serial_list()

Output window will print the returned information:

COM ports name is “COM11”.

Using serial_open() command, we can check if this port is ready for transmission of data. We will use the most common serial port parameter setting: 9600–8-N-1 (9600 bits per second, eight (8) data bits, no (N) parity bit, and one (1) stop bit)

=serial_open("COM11",9600,8,0,1,0)
;baudrate at 9600
;8 data bits
;1 stop bits
;none parity
;no flowcontrol

Output window will print the returned information. Remember, this function’s return value gives 0, when port is successfully opened.

Great, now that this is settled, we can close it and we can prepare the data which will be sent to the relay board in order that we obtain board’s address ID.

 

Request message and response message data for reading address ID of relay board(as per user manual):

Request message HEX: 0x00 0x03 0x00 0x00 0x00 0x01 0x85 0xDB 
  First byte [0x00] is slave device address ID. But since we have no way of knowing it(board does not use DIP switches), we can use reserved value of "00".
    This means that all devices(or only one) on the bus will receive and acknowledge this data and give suitable response. 
 
  Second byte [0x03] is function code
    Function code in Modbus is a specific code used in a Modbus PDU to tell the Modbus slave device what type of memory (i.e. holding registers, input coils, etc)
    to access and what action to perform on that memory (i.e. reading or writing). This PDU uses function code 3, which is used for reading holding registers.
    Holding registers are the most universal 16-bit register, may be read or written, and may be used for a variety of things including inputs, outputs, configuration data, or any requirement for "holding" data.
  
  Bytes 3 to 6 [0x00 0x00 0x00 0x01] are starting register address and the number of registers we want to read. 
  
  Last two bytes [0x85 0xDB] are CRC 16-bit calculated value. CRC 16-bit value is first calculated and then appended to the end position of array.

Response message HEX: Expected return data should be 0x00, 0x03, 0x02, 0x00, 0x01, 0x44, 0x44. 
5th byte of the return data is boards address ID -> HEX: 0x01 ; DEC: 1

Whole code for reading board’s address ID:

=.payload = array_new()
array_setdata(.payload, 0, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01)
serial_open("COM11", 9600, 8, 0, 1, 0)
.crc = array_crc16(.payload, 0, -1)
array_setdata16(.payload, -1, .crc)
serial_writearray("COM11", .payload)
array_clear(.payload)
serial_readarray("COM11", .payload, 150)
serial_close("COM11")
.rc = array_getdata(.payload, 4)
print('Address ID:', .rc)
array_delete(.payload)
Description of used functions: 
=.payload = array_new()  -> new array with handle .payload is created
array_setdata(.payload, 0, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01) -> we set data to .payload array
serial_open("COM11", 9600, 8, 0, 1, 0) -> opens COM port and sets parameters
.crc = array_crc16(.payload, 0, -1) -> calculates CRC 16-bit value of .payload array data
array_setdata16(.payload, -1, .crc) -> we append 16-bit CRC data at the end position of .payload array
serial_writearray("COM11", .payload) -> sends .payload array data via COM11 port
array_clear(.payload) -> we clear .payload array data
serial_readarray("COM11", .payload, 150) -> we read the response data from relay board and set it to .payload array
serial_close("COM11") -> closes COM11 port
.rc = array_getdata(.payload, 4) -> we set the 5th byte (zero type numbering is used) of received data into .rc variable
print('Address ID:', .rc) -> prints .rc value
array_delete(.payload) -> deletes .payload array handle

 

Relay On/Off control

Now that we know board address ID, and since this is a relay board, lets control one of the relays on/off.
Request message and response message data for relay No. 1 ON(as per user manual):

Request message HEX: 0x01 0x05 0x00 0x00 0xFF 0x00 CRC-byte CRC-byte 
  First byte [0x01] is slave device address ID. As we know, address ID of relay board is 1.

  Second byte [0x05] is function code. Function code in Modbus is a specific code used in a Modbus PDU to tell the Modbus slave device what type of memory (i.e. holding registers, input coils, etc) to access
    and what action to perform on that memory (i.e. reading or writing). This PDU uses function code 5, which is used to write a single output to either ON or OFF in a remote device.
 
  Bytes 3 to 6 [0x00 0x00 0xFF 0x00] are coil address(first relay at 0...) and the constant value for ON 0xFF 0x00, and a constant value for OFF 0x00 0x00. 
 
  Last two bytes are CRC 16-bit calculated value. These two bytes are not set together with first six bytes. CRC 16-bit value is first calculated and then appended to the end position of array.

Whole code for turning relay 1 ON:

=.payload = array_new()
array_setdata(.payload, 0, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00)
serial_open("COM11", 9600, 8, 0, 1, 0)
.crc = array_crc16(.payload, 0, -1)
array_setdata16(.payload, -1, .crc)
serial_writearray("COM11", .payload)
array_clear(.payload)
serial_readarray("COM11", .payload, 150)
serial_close("COM11")
.rc = array_getdata(.payload, 4)
array_printdata(.payload)
print('Address ID:', .rc)
array_delete(.payload)

Whole code for turning relay 1 OFF:

=.payload = array_new()
array_setdata(.payload, 0, 0x01, 0x05, 0x00, 0x00, 0x00, 0x00) 
serial_open("COM11", 9600, 8, 0, 1, 0) 
.crc = array_crc16(.payload, 0, -1) 
array_setdata16(.payload, -1, .crc) 
serial_writearray("COM11", .payload) 
array_clear(.payload) 
serial_readarray("COM11", .payload, 150) 
serial_close("COM11") 
.rc = array_getdata(.payload, 4) 
array_printdata(.payload) print('Address ID:', .rc) 
array_delete(.payload)

 

Implementing Modbus communication within TNG software

Executing these commands from MDI is nice for testing, but not really practical in real application. Best way to implement MODBUS communication within the TNG would be to write an expression function which would inhibit all necessary code for master modbus <-> slave communication.

To read more about Expr file and its properties, you can follow link below:
CNC machine semaphore application using Expressions (Expr.txt)

 

 

Create new expr file and name it e.g. Expr_MB_relay.txt. We can include two expression functions in this file, one for relay ON and other for relay OFF.
Lets name them #RelayControl_ON and #RelayControl_OFF.  Include the corresponding code for each function.

The code of Expr_MB_relay.txt file would look like this:

#RelayControl_ON
.payload = array_new();
array_setdata(.payload, 0, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00);
serial_open("COM11", 9600, 8, 0, 1, 0); 
.crc = array_crc16(.payload, 0, -1); 
array_setdata16(.payload, -1, .crc); 
serial_writearray("COM11", .payload); 
array_clear(.payload); 
serial_readarray("COM11", .payload, 150); 
serial_close("COM11"); 
.rc = array_getdata(.payload, 4); 
array_printdata(.payload);
print('Address ID:', .rc); 
array_delete(.payload);

#RelayControl_OFF
.payload = array_new();
array_setdata(.payload, 0, 0x01, 0x05, 0x00, 0x00, 0x00, 0x00); 
serial_open("COM11", 9600, 8, 0, 1, 0); 
.crc = array_crc16(.payload, 0, -1); 
array_setdata16(.payload, -1, .crc); 
array_writearray("COM11", .payload); 
array_clear(.payload); 
serial_readarray("COM11", .payload, 150); 
serial_close("COM11"); 
.rc = array_getdata(.payload, 4); 
array_printdata(.payload)
print('Address ID:', .rc); 
array_delete(.payload);

 

You can run expression functions either from .NC file, script file, button file, M code file, expression file etc..

Now, if you would like to execute these two functions within a .NC program or gcode script file, you can use (expr, ) comment function:

%
.
.
.
G00 X0 Y20
G01 Z5
(expr,exec("#RelayControl_ON"))
G01 Z-1
G00 Z5
(expr,exec("#RelayControl_OFF"))
.
.
.
%

 

While both examples of Modbus functions described above would work, it would not be a good example of how such communication should be handled.

For each request data sent, we also need to read the response data. Response is an automated process from slave device, and we can use the response data in order that we can verify that request procedure has been acknowledged, processed and correctly executed. In next part we will describe how to correctly write more reliable expression code for Modbus communication.

 

Continued at part 4:
https://planet-cnc.com/serial-communication-with-planetcnc-tng-part-4/

 

 

 

 

 

 

Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 2

We will continue with expression functions used for serial communication. In first part we described array expression functions, which are used to prepare the data for serial communication. Now we will describe serial expression functions that are responsible for actually sending the data.

serial_list()    - Displays all available COM ports of computer. 

serial_info(port) - Displays additional info of COM port of computer
  port - COM port name. Use string type ; e.g. "COM3"

serial_open(port,baudrate,bits,parity,stopbits,flowcontrol) - Opens serial COM port channel and sets serial communication parameters
  port - COM port name. Use string type ; e.g. "COM3"
  baudrate - baudrate value, e.g. 4800bd ; 9600bd ; 19200bd ; 115200bd 
  bits - data bits value, 5 ; 6 ; 7 ; 8 ; 9 data bits
  parity - parity value, 0 - no parity ; 1 - odd parity ; 2 - even parity
  stopbits - stop bit value , 0 - no stop bit ; 1 - one stop bit ; 2 - two stop bits
  flowcontrol - flow control value; 0 - no flow control ; 1 - flow control enabled
  Return value: Returned value is zero when port is successfully opened. Returned value is 1 when port has not been opened. Returned value is integer number. 

serial_config(port,baudrate,bits,parity,stopbits,flowcontrol) - Configures serial COM port channel communication parameters
  port - COM port name. Use string type ; e.g. "COM3" 
  baudrate - baudrate value, e.g. 4800bd ; 9600bd ; 19200bd ; 115200bd 
  bits - data bits value, 5 ; 6 ; 7 ; 8 ; 9 data bits 
  parity - parity value, 0 - no parity ; 1 - odd parity ; 2 - even parity 
  stopbits - stop bit value , 0 - no stop bit ; 1 - one stop bit ; 2 - two stop bits 
  flowcontrol - flow control value; 0 - no flow control ; 1 - flow control enabled
  Return value: Returned value is zero when port is successfully configured. Returned value is 1 when port has not been configured. Returned value is integer number. 

serial_close(port) - Closes serial COM port channel
  port - COM port name. Use string type ; e.g. "COM3" 
   Return value: Returned value is zero when port is successfully closed. Returned value is 1 when port has not been successfully closed. Returned value is integer number. 

serial_write(port,str) - Sends string type data trough serial COM port
  port - COM port name. Use string type ; e.g. "COM3" 
  str - String type data that will be sent ; String data should be enveloped in quotes
  Returned value: Return value is data size. Returned value is integer number.
  
serial_writedata(port,data) - Sends byte type data trough serial COM port
  port - COM port name. Use string type ; e.g. "COM3" 
  data - Byte type data that will be sent
  Returned value: Return value is data size. Returned value is integer number.

serial_writearray(port,hnd) - Sends array data trough serial COM port
  port - COM port name. Use string type ; e.g. "COM3" 
  hnd  - array handle  
  Return value: Return value is data size. Returned value is integer number.

serial_read(port,size,timeout(optional)) - Reads data trough serial COM port and prints it at the output window. 
  port - COM port name. Use string type ; e.g. "COM3" 
  size - data size limit to be read; 0 -> no limit in data size, 256 -> Max value of limit 
  timeout - timeout value in milliseconds. After time has elapsed, COM port will not read any incoming data. This argument is optional. 
  Return value: Return value is data size. Returned value is integer number.

serial_readdata(port,size,timeout(optional)) - Reads data trough serial COM port and prints it at the output window. 
  port - COM port name. Use string type ; e.g. "COM3" 
  size - data size limit to be read; 0 -> no limit in data size, 256 -> Max value of limit
  timeout - timeout value in milliseconds. After time has elapsed, COM port will not read any incoming data. This argument is optional. 
  Returned value: Return value is data size. Returned value is integer number.

serial_readarray(port,hnd,size,timeout) - Reads data trough serial COM port and saves it to array
  port - COM port name. Use string type ; e.g. "COM3" 
  hnd - array handle
  size - data size limit to be read; 0 -> no limit in data size, 256 -> Max value of limit
  timeout - timeout value in milliseconds. After time has elapsed, COM port will not read any incoming data. 
  Return value: Return value is data size. Returned value is integer number.

Now its time for a simple serial communication example. For this we will use two COM ports, terminal program and PlanetCNC TNG software.  Such approach is suitable for getting comfortable with  array and serial functions on a somewhat real communication case.

 

First lets check for available COM ports of our computer.  If your computer does not offer any physical serial COM port, you can use virtual COM port software.

Under Control Panel/Device Manager/Ports(COM&LPT) you can easily check available ports:

 

Same can be done in TNG software using a serial_info() command, which prints the following info:

 

I will use the COM4 and COM5 ports for communication between the TNG software and terminal program.

TNG software will be “attached” to COM5 and terminal program will be “attached” to COM4. In order that the communication channel between both ports is established, we need to connected COM4 and COM5 with each other.  Transmit line of COM4 is connected at the receive input of COM5, and transmit line of COM5 is connected at the receive input of COM4. So anything that will be sent at the COM5 end will arrive at the COM4 receiving end and vice versa.

 

Terminal program configuration:

HTerm terminal program is attached to COM4 port, communication parameters are set:

– baudrate at 9600

– 8 data bits

– 1 stop bits

-none parity

– no flowcontrol

 

Since both devices on serial bus need to be configured the same, we will use same port configuration in TNG using function: serial_open():

serial_open(“COM5”,9600,8,0,1,0)

It is mandatory that afters finished transmission, port is closed using serial_close() function.

For start, we just want to say hello to Hterm program, best way to do it is to send string data using serial_write() function.

So, our short MDI program would look like this:

 

At the Hterm’s end we receive the message:

 

It would only be appropriate that we answer the call and reply with: Hello PlanetCNC!

However, we need to make sure that PlanetCNC is ready to receive the call and is in “listening” mode, this is done using a serial_read() function:

Timeout value will open the listening window for 4 seconds, which gives more than enough time for Hterm to send the message:

Since this function only reads and prints the string data, we should be able to see the message in our output window:

 

But usually devices don’t talk to each other using string data for saying hello, but they use data in some structured manner.

This will all make more sense in the next chapter where we will use MODBUS to communicate with relay board.

 

Last example will describe a short and simple correspondence where we will send array data and set received data into array.

-new array with handle name hnd is created:

=hnd =array_new()

-arraydata is set with array_setdata() function:

array_setdata(hnd,0,0xA1,0xB2,0xC3)

-serial port is opened and configured:

serial_open("COM5", 9600, 8, 0, 1, 0)

-array data is sent:

serial_writearray("COM5",hnd)

-array is cleared and ready for incoming data:

array_clear(hnd)

-data is read and set to array

serial_readarray("COM5",hnd,5000)

port is closed and array is deleted, data is printed:

serial_close("COM5")
array_printdata(hnd)
array_delete(hnd)

Hterm received data:

Sent data from terminal:

 

Output window displays the contents of hnd array:

 

Continued at part 3:
https://planet-cnc.com/serial-communication-modbus-relay-board-with-planetcnc-tng-part-3/

 

 

 

 

 

 

 

 

 

Serial communication(MODBUS relay board) with PlanetCNC TNG – Part 1

This series of tutorials will try to explain and demonstrate how to use PlanetCNC TNG software for the means of serial communication.

The final goal would be a successful control of external MODBUS input/output relay board, but, to achieve this, we need to start with the basics.

 

PlanetCNC TNG software possesses all the tools for serial communication with external equipment. PlanetCNC motion controllers also support hardware UART serial interface, but this will not be a subject of this tutorial, at least not in this part.

While serial communication comes with its own rules, requirements and data framing, you would not need to worry for any of that. TNG already provides all required commands that implement serial specification so that you can just relax and read/send the data on command so to speak.

Since serial communication is associated with relatively large amount of data transfer and to make manipulation of this data easier, there is a special group of functions with a prefix array.

 

Array expression functions:

These functions are important tool for data manipulation, since serial communication requires correct data flow in a sense of sending/reading  structured data to/from designated  addresses of external devices. In short, these functions will help you to prepare the data you wish to send.

array_new() - Creates new array. Returns handle to newly created array. 
 Return value: Handle of newly created array. Handle is integer number.

array_delete(hnd) - Deletes array referenced by handle. 
  hnd - array handle
  Return value: Returns handle of the array that was just deleted, or zero if array does not exist. Returned value is integer number.  

array_clear(hnd) - Clears array data. 
  hnd - array handle
  Return value: Returns handle of the array that was just cleared, or zero if array does not exist. Returned value is integer number.

array_isvalid(hnd) - Checks if array with this handle is valid.
  hnd - array handle
  Return value: Returns value 1 for valid array, returns value 0 for invalid array. Returned value is integer number.

array_size(hnd) - Returns array data size. 
  hnd - array handle
  Return value: Returns array size. Returned value is integer number.
array_setstring(hnd, str) - Replaces array with string. Old data is cleared, array now contains only string data.  
  hnd - array handle 
  str - string data
  Return value: Returns array size. Returned value is integer number.
array_printstring(hnd) - Prints string of specified array. 
  hnd - array handle
  Return value: Returns array size. Returned value is integer number.
array_setdata(hnd,pos,data) - Sets byte data to array. Old data at position is cleared, array now contains new data. If new data is set at the end of array, data is appended.  
  hnd - array handle
  pos - data position ; 0: -start position of array -1:-end position of array ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) )
  data - byte data
  Return value: Returns array size. Returned value is integer number.
array_setdata16(hnd,pos,data) - Sets two byte(16-bit) data to array. Old data at position is cleared, array now contains new data. If new data is set at the end of array, data is appended. 
  hnd - array handle
  pos - set data position ; 0: -start of array -1:-end of array ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) )
  data - two byte data
  Return value: Returns array size. Returned value is integer number.
array_setdata32(hnd,pos,data) - Sets four byte(32-bit) data to array. Old data at position is cleared, array now contains new data. If new data is set at the end of array, data is appended. 
  hnd - array handle 
  pos - set data position ; 0: -start of array ; -1:-end of array ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) ) 
  data - four byte data
  Return value: Returns array size. Returned value is integer number.
array_printdata(hnd) - Prints array data. 
  hnd - array handle
  Return value: Returns array size. Returned value is integer number.
array_crc16(hnd,pos,len) - Calculates 16-bit CRC number.
  hnd - array handle
  pos - CRC data start position ; 0: -start of array; Other: 1,2,3...zero based numbering is used (1st position has number zero, 2nd number one etc…) )
  len - CRC data length value ; -1:-end of array ; Other: 1,2,3...
  Return value: 16-bit CRC number. Value is integer number
array_crc32(hnd,pos,len) - Calculates 32-bit CRC number.
 hnd - array handle
 pos - CRC data start position ; 0: -start of array ; Other: 1,2,3...(zero based numbering is used (1st position has number zero, 2nd number one etc…) )
 len - CRC data length value ; -1:-end of array ; Other: 1,2,3... 
 Return value: 32-bit CRC number. Value is integer number
array_getdata(hnd,pos) - Returns byte data from array position. 
  hnd - array handle  
  pos - array data position ; 0: -first position of array ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) )
  Return value: Return value is byte data. Value is integer number.
array_getdata16(hnd,pos) - Returns two byte data from array position.
  hnd - array handle 
  pos - array data position ; 0: -first position of array ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) )
  Return value: Return value is two byte data. Value is integer number.
array_getdata32(hnd,pos) - Returns four byte data from array position.
  hnd - array handle 
  pos - array data position ; 0: -first position of array ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) )
  Return value: Return value is four byte data. Value is integer number.
array_copy(hnd,from,pos,start,len) - Copies data from one array to another. 
  hnd - Destination array handle. Array to which we copy the data 
  from - Source array handle. Array from which we copy the data 
  pos - Destination position of copied data ; 0: -first position of array; -1: -end position ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) )
        If data exist it will be replaced with new data for its length. If position is -1(end position of array), data will be appended. 
  start - Source start position of data to be copied ; 0: -first position of array ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) ) 
  len - Length of the copied data 
  Return value: Return value is destination array size. Returned value is integer number.

array_resize(hnd,newsize) - Resizes array. 
  hnd - array handle of to be resized array
  newsize - new array size. When resizing, data can be added or removed. Negative number will preappend/remove the data at the start position of array and positive number will append/remove the data at the end position of array
  Return value: Returned value is new array size. Returned value is integer number. 

array_insert(hnd,pos,len) - Inserts byte data filled with zeros to existing array.
  hnd - array handle 
  pos - position of inserted data ; 0: -first position of array -1:-last position ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) )
  len - length of inserted data
  Return value: Return value is array size. Returned value is integer number.

array_remove(hnd,pos,len)- Removes byte data from existing array 
  hnd - array handle
  pos - position where we want to remove the data ; 0: -first position of array ; Other: 1,2,3... (zero based numbering is used (1st position has number zero, 2nd number one etc…) )
  len - length of removed data
  Return value: Return value is array size. Returned value is integer number.

Example:

We would like to set a string data type(sequence of characters), to array with a handle name hnd. String data would be: PlanetCNC

First we need to create a array. Then we need to assign it a handle with a name hnd. Don’t forget to delete this array at the end of its use.

hnd = array_new()

In this array we need to copy string “PlanetCNC”:

array_setstring(hnd,"PlanetCNC")

We can  check the arrays data if we print its value in the output window:

array_printstring(hnd)

It is mandatory to delete the array handler at the end of its use:

array_delete(hnd)

You can use MDI input field to execute functions above. To display the expression evaluation use output window:

 

Let’s try to do this same example in a slightly different way. Since we usually want to communicate with different devices that usually provide the data in e.g. HEX type, we will set ASCII HEX representation values of the characters and achieve the same result.

To find the ASCII HEX values, use ASCII table for reference or online text to HEX converter.

The main difference is that instead of using array_setstring(), we used array_setdata() function:

Since we used HEX representation values of string PlanetCNC, array_printstring() will still print PlanetCNC:

 

To our string of characters “PlanetCNC”, we want to add HEX representation of characters ” rules!” Final text would be “PlanetCNC rules!” 

 

Here, is where we append additional data(” rules!”) to the array. Second argument “-1” defines the location of newly added data. -1 means it will be added at the end of the array.

 

With array_printdata(), we print all HEX values of array:

 

If we want to obtain desired data from a specific array location, we can use the array_getdata() function. Say we want to read the fourth byte of hnd array:

Output window will print the value of fourth byte. Decimal value 110 is equal in HEX 0x6E.

 

When sending data, there is a possibility of data corruption or data change. Receiver would somehow need to validate that the received data is indeed the one that sender intended to send.

For this purpose CRC code is used. CRC stands for “Cyclic redundancy check”, and it represents our data payload, in a form of calculated/encoded number, that is appended at the end of our data message. This way receiver can calculate the CRC of  received payload data and compare received CRC with the one it calculates on its own.

If everything is ok, both CRC numbers should be identical.

 

To generate 16-bit CRC value of the hnd array data, we can use array_crc16() function and save CRC number in the rc variable:

hnd represents the array that CRC16 function will calculate CRC number of

0 represents the start position of hnd array data

-1 represents the length or width of data to be calculated. Since -1  represents the end position of array, this means that we will calculate all data of hnd array.

rc is the variable to which we will save our CRC number.

 

Now we just need to append the 16-bit CRC value to our hnd  array and data is ready:

Since we want to append the two byte data to our array, we need to use array_setdata16() function.

hnd represents array that we will append data to

-1 represents the position of array where we will append data. -1 means that we will append it at the end position of array

rc is the variable that contains calculated CRC number, and it represents the appending data.

 

 

In the next part of this tutorial series we will describe function group for serial communication.

Serial expression functions:

serial_list - 
serial_info -

serial_open - 
serial_config - 
serial_close -

serial_write - 
serial_writedata - 
serial_writearray -

serial_read - 
serial_readdata - 
serial_readarray -

Continued at part 2:
https://planet-cnc.com/serial-communication-with-planetcnc-tng-part-2/

 

PlanetCNC wireless handwheel button customization

User can customize handwheel button functionality. This can be done by configuring the Handwheel.txt file.

User manual for wireless handwheel is available here: Wireless handwheel user manual

 

First, create a blank Handwheel.txt file and place it in your TNG profile root folder. You can create such file with any text editor. How to configure this file will be described further in this document.

 

Button HEX values:

Each button of the handwheel keypad is represented by its default HEX value. These values could also be in a decimal form, but HEX gives better and more intuitive
description. Each button can be represented with two types of HEX values, Lower HEX (SHIFT button is not active) and Upper HEX values (SHIFT button is active).

 

Best to describe it is by using illustrated view:

Lower HEX button values (SHIFT button not active):

 

While holding SHIFT button, each buttons is now represented with the Upper HEX value:

Now that we know LOWER and UPPER values of each button, we can easily assign new
alternative functions of the handwheel buttons.

 

 

BASIC Handwheel.txt file configuration:

Earlier chapter can be better described with the actual command lines of the configuration
file.

Below are a default* program functions that will be executed when corresponding
button will be activated:

cmd: "Machine.Emergency_Stop" "" num=0x0001
cmd: "Machine.Start" "" num=0x0002
cmd: "Machine.Pause" "" num=0x0003
cmd: "Machine.Work_Position.Offset.To_Zero" "" num=0x0004
cmd: "Machine.Work_Position.Axis_To_Zero.XY" "" num=0x0005
cmd: "Machine.Work_Position.Axis_To_Zero.Z" "" num=0x0006
cmd: "Machine.Flood" "" num=0x0007
cmd: "Machine.Work_Position.Measure_Height" "" num=0x0008
cmd: "Machine.Tool_Offset.Measure_Length" "" num=0x0009
cmd: "Machine.Move.Axis_To_Zero.XY" "" num=0x000a
cmd: "Machine.Spindle" "" num=0x000b

*(Note that this is a default behaviour of handwheel buttons. No configuration file or
configuration of it is needed to obtain it… )

 

Lines below will execute Homing procedure, Machine Stop and Mist ON/OFF actions.
Becuse upper HEX values are used, this indicates that SHIFT button needs to be pressed:

Homing procedure:
cmd: "Machine.Home" "" num=0x0a00

+

Machine Stop:
cmd: "Machine.Stop" "" num=0x0300

+

Mist On/Off:
cmd: "Machine.Mist" "" num=0x0700

+

So if user wishes to add an alternative functionality to a button, all it needs to be done is to add a line which would execute desired program function.

Upper HEX value of button should be used.

 

Example:
We would like to execute Surface Measure procedure when we press the Start button in combination with SHIFT button.
Since we will assign an alternative button functionality we need to obtain the Upper HEX button value.

Earlier chapter with the layout illustration gives this information → 0x0200

Cmd line is usually the menu path of the program function. In this case this would be:
Machine/Measure/Surface

Note that path menu levels are represented with “.” and spaces in the program function name with “_” character.

Command line that would help us achieve this is: cmd: “Machine.Measure.Surface” “” num=0x0200

If we would like to turn ON/OFF Output 1 with the same button instead, we would write: cmd: “Machine.Outputs.Output_1” “” num=0x0200

 

ADVANCED Handwheel.txt file configuration:

Since there are not enough buttons for every possible function of tool that TNG offers, macros can be created. Macros are able to execute program functions, expression
functions, parameter manipulation etc. They can be navigated and used trough the Custom menu.

How to create a macro:
Just as we added command lines for button alternative functions to Handwheel.txt file, in the same way we add dedicated lines to create macros.
We will demonstrate three examples of macro use. Macro for program function, program function with parameter manipulation and expression function.

 

Macro command line for program function File Open:

cmd: "File.Open_..." "Open file" num=0x10001

cmd: “File.Open_…”
Like before, command line is the path of the program function, File/Open

“Open file”
Macro will be displayed under this name in the custom menu.

num=0x10001
This num value serves as a macro ID as also as a menu sequence number. First value 1 identifies this as a macro line, and last value 1 is its sequence number.

 

Macro command line for expression function with parameter manipulation:

cmd: "Machine.Output_PWM" "PWM" num=0x10002 param=0|10|0|100 val="[0;expr:_hw_mpg_custom]"

cmd: “Machine.Output_PWM”
Like before, command line is the path of the program function, Machine/Output_PWM

“PWM”
Macro bill be displayed under this name in the custom menu.

num=0x10002
This num value serves as a macro ID as also as a menu sequence number.First value 1 identifies this as a macro line, and last value 2 is its sequence number.

param=0|10|0|100
These are definition values of PWM signal that will be changed trough this macro. So, when the PWM signal will be modified, if will consider these definitions:

0[initial value] | 10[increment value] | 0[min value] | 100[max value]

val=”[0;expr:_hw_mpg_custom]”
0 is a number of output pin that will generate the PWM signal. Zero based numbering is used (1st pin has number zero, 2nd pin has number one etc…)
Macro uses parameter _hw_mpg_custom for PWM signal value modification.

 

Macro command line for expression function:
This macro executes expression function located in the Expr.txt file.

expr: "exec('#Message')" "Message" num=0x10003

expr: “exec(‘#Message’)”
Since exec is an expression function, we need to use expr command at the beginning of the macro line. exec command will execute the #Message function located in our Expr.txt file.

In this case, #Message function looks like this:
#Message
exec(msg(‘Handwheel function trigger’));

“Message”

Macro bill be displayed under this name in the custom menu.

num=0x10003
This num value serves as a macro ID as also as a menu sequence number. First value 1 identifies this as a macro line, and last value 3 is a sequence number

 

Accessing and using the Custom menu
Custom menu can be accessed using axis switch knob set at C position:

 

 

At first, display will show current jogging mode info. If the handwheel encoder is rotated custom menu will be displayed.

As per our configuration, three macros will be available for selection. Using a handwheel encoder, user can navigate trough the menu items:

To execute the Open File or Message macro, macro should be selected in the menu and confirmed with Cycle button:

 

To execute PWM macro, we select it in the menu and by simultaneously using a Shift button and an encoder wheel we can change the PWM duty cycle value:

To actually set PWM on the output pin, we just need to confirm it with the Cycle button:

Configuring Python path in PlanetCNC TNG software

With PlanetCNC TNG software you can use Python scripts. This gives a user a whole lot of new possibilities on how to customize or accommodate his CNC application for really specific requirements.

PlanetCNC TNG and Python relation

  • Python(in further text – PY) is a programming language known for its simplicity and large standard library.
  • PlanetCNC TNG sw can use PY to extend its functionality.
    • In most cases as a custom gcode generators or control entity or external data processing.
  • If we want to equip TNG with this new PY functionality, we need to install the PY interpreter to our computer.
    • We recommend that installed version is embeddable PY since this makes it a dedicated interpreter only for needs of TNG sw.
    • Of course you can choose also system version of PY interpreter.
    • Either way, be attentive to 32-bit or 64-bit version that you intend to use, depending on your OS

How does it work?

  • TNG issues a request to PY interpreter for a script execution. PY script code uses dedicated PlanetCNC py commands, and that is basically how the PY interpret communicates with the TNG. These commands are inhibited in two dedicated TNG PY modules: gcode and planetcnc
  • PY scripts can be directly executed, they can be imported or you can run them via Expr, Gcode commands, Toolbar Buttons.
  • Not all scripts can be imported, this is conditioned by types of commands used in the script code.
  • Available documentation:
    • TNG User Manual -> PY related program features related settings
    • G-code ref manual -> PY expression functions, PY modules and dedicated commands
    • Samples folder (located at installation directory in PlanetCNC/Samples folder )
      • Recommended examples:
        • Lissajous.py
        • Ripple.py
        • Laser Photo.py
  • NOTE: Do not confuse TNG’s internal embedded python interpreter with TNG PY API.
    • API’s main purpose is to control TNG from external app, while TNG internal embedded PY interpreter is to run PY scripts within the TNG.

 

Configuring the path to the Python interpreter in the TNG sw

First step using Python with TNG, is to install the PY interpreter. TNG supports PY versions from 3.5 to last version (current last version is 3.11.)

You can download Python from the link below, as recommended, choose embeddable package:

https://www.python.org/downloads/windows/

 

Below is an example of Windows embeddable version of Python release 3.9.13:

 

When download is finished, copy and paste the archive file into your main Profiles folder.

Then extract it in the same folder. What you will end up with is a python folder with all of its dedicated files.

File, to which we need set path in the TNG sw is python39.dll (if you would use 3.10 version, then this file would be python310.dll etc):

Path:

 

Now open TNG software, and under File/Settings/Program Options/Paths insert path to the python.dll file:

 

After you click Check button, you should receive on-screen message regarding installed Python version:

 

If something went wrong, software will notify you python or file is not available.

 

After successful installation and path config, you will be able to see that PY related menu options are now available:

 

 

CNC machine semaphore application, using expressions (Expr.txt) – Part 4

In the last part of this tutorial series, we will connect the ExtInOut board with our controller and test the functionality of our Expr.txt file, finally!

Before we continue, I would recommend that you go trough the user manual of ExtInOut board for more info about the hardware requirements and connections.

ExtInOut board user manual

 

Of all states, Alarm state is the most “complex”(not really) and for that more interesting, so again just a quick detailed explanation or alarm state behavior. I might add that I added additional requirements:

  • Additional evaluation of second and third ExtInOut input (in this case, second input will simulate a pressure sensor, third will be used as  a “Error reset” button)
  • NO Pressure” error state event can be triggered in Run state as also in Idle state (while Door error state event can be triggered only during the Run state). To clarify, Alarm state transition condition can from now on be triggered either by door switch or pressure sensor.
  • While in Alarm state, dedicated light will toggle with approx 1Hz period
  • At Alarm state event, on-screen message will appear, notifying the user of error description (either “Open Door” or “NO Pressure”). Error description message will also be displayed at the status bar.
  • At alarm  state event, siren sound will activate and notify the user for better alertness
  • If user will turn off the machine while Alarm state still being active, then with the next machine start-up, Alarm state will still persist with all of its properties(light toggle, program run blockage etc) and user will need to reset it (if the conditions for reset are met). At launch, status bar will notify the user of last Error description before machine was shutdown.
  • User will not be able to start a NC program until Alarm state is cleared (pressure sensor or door switch ok) or previous alarm state has not been reset

 

Expr.txt file can be downloaded below:

Expr.txt

 

Commentary is added for better explanation and understanding.

 

Connection of controller, ExtInOut board, input switches and LED’s of semaphore

In my case, I will use a dedicated keypad for input switch simulation and LED’s for semaphore lights.  Note that I did not find all suitable colors for my lights, so some of them will the same color.

 

So, we have three input switches/buttons, connected at:

Input 1 -> Simulates open door switch

Input 2 -> Error reset button

Input 3 -> Simulates pressure sensor

 

LED’s for indication of active states will be connected to relays:

Relay 1: IDLE state

Relay 2: RUN state

Relay 3: PAUSE state

Relay 4: ALARM state

Relay 5: ESTOP state

 

 

Picture of the demo setup:

So, lets test the Expr.txt semaphore application.

I will add some screenshots that will show the behavior of the Expr. txt.

 

Import program:

Since no pre-existing error events are present, status bar displays “No error” text.

ExtOut output 1 is activated already at the machine startup, indicating IDLE state:

 

Start program:

We start the program, and ExtOut output 2 is activated:

 

Pause Event:

Pause state is activated via M00 command in NC program:

This activates also ExtOut output 3 next to already active output 2:

Error “Door Open” Event:

While in Pause state, I simulate door switch activation which triggers the Door Open alarm state. Siren sound is played trough speakers, program execution is stopped and message is displayed:

ExtOut output 4 starts to toggle:

and status bar displays the error type:

 

Reset Error before Start program:

While in alarm state, I want to start the program again, but, since the error has not been reset, I receive notification message asking me to reset the error, and the number of alarm (1- Door error, 2- Pressure error):

After using alarm reset button, status bar displays no error:

IDLE state is again reinstated:

Error “NO pressure” Event:

So, I start the program again, and at one point I simulate pressure sensor, siren sound plays, program execution is stopped and software will display the “NO PRESSURE” message:

Status bar displays the error type:

 

Shutdown and startup of the system:

This time, I did not reset the alarm, but I just shutdown the machine.

At machine startup, I receive the “INIT: Please Reset Error” message. At this point, machine is still in Alarm state.

Status bar notifies me of the last error that was not reset:

Note that pressure error event can be activated in IDLE as also RUN state. This is because, user should be notified right at the machine startup as also in idle state that the air pressure is not present and it should be handled appropriately. Error will not be reset if pressure sensor is still activated(i.e. no pressure detected).

Estop:

At any point during of any other state, if Estop is triggered, ESTOP state will be activated. Estop does not reset the alarm state, so once the estop is released, alarm state will persist.

 

Short demonstration video:

Gantry square procedure with PlanetCNC

Many machines use two lead/ball screws or rack and pinion rods per machine axis. In most cases, the Y axis.
Rods can be driven with one motor or two. One motor usually drives both rods using a timing belt, which also helps with
synchronization.

But you can also drive each rod with its separate motor. At PlanetCNC we use the terms Master and Slave motor.
PlanetCNC TNG software takes care of master and slave motor synchronization internally.

Nevertheless, between the two motors, there might be some discrepancy in a sense of gantry squareness. This can be due motor failure, lost steps, mechanical problems etc..

Since CNC machines are meant to ease the pain of accuracy and repeatability of machining process, we as users always strive
for squareness of our CNC machine.

In order to achieve this with dual motor setup, PlanetCNC TNG sw offers Gantry Square feature.

What gantry square feature does,  it “decouples” the master and slave axis motors from their common axis (e.g. Y), and assigns them new axis letters U and V, for the purpose of gantry square procedure. This needs to be done so that system is able to detect limit switches of each motor, as also to separately control each motor in order to  align gantry.

 

Before you proceed, we recommend that your machine steps per unit and limit switch configuration is set correctly:
Steps per unit tutorial
Limit switch tutorial

1. Setting the motors of Master and Slave axis

Under File/Settings/Motors we set additional (slave) motor of desired axis. In our case, axis output 4 will be used.

 

2. Settings Limit switches

Configuration of limit switch inputs for master and slave motor. We need to set limit switch inputs under motor U and V.

Motor U will have the same input number as motor Y, while motor V will have input number 4.

 

 

3. Gantry square parameters

Here we set the parameters of gantry square procedure.


Speed: 

Speed value at which gantry square procedure will be performed.

 

Axis:

Machine axis that will be squared. In our case Y.

 

Direction: 

Direction of squaring axis motion. It should be the same direction as set for limit switches for U and V.

 

Move U:

Value of measured misalignment of master axis. This is the distance for which master motor will retract.

 

Move V:

Value of measured misalignment of master axis. This is the distance for which slave motor will retract.

 

4. Using the gantry square procedure

Gantry square procedure will consist of two takes.

For first take we will use Move UV values set to zero. So, if any misalignment of limit switches (gantry) exists (and it probably does), we will measure it trough a test. In second take we will use these measured values as Move UV values, and therefore square our gantry.

 

We start the procedure under : Machine/Measure/Gantry square

Machine will move in direction set under Direction. Once the first limit switch is activated, corresponding motor will stop, second motor will continue to move its side of the gantry until the second limit switch is activated. After activation, second motor also stops.

After first take, mill or engrave a square and measure the discrepancy of the angle. One user recommended the use the 3-4-5 Triangle approach. Trough measurement or calculation, we can obtain the Move UV correctional values and use it with second take.

 

Image below demonstrates (exaggerated) situation of limit switch misalignment.

-Normal line represents gantry misalignment due to limit switch

-The doted line represents squared gantry.

 

Short demonstration video:

CNC machine semaphore application, using expressions (Expr.txt) – Part 3

At this step of the process we pretty much defined everything needed for semaphore application and we can begin writing the program.

Diagram of program

First we will illustrate simple program diagram(flowchart). Diagram is very helpful, because it visually  illustrates the program flow(in this case state machine) which we can use as a reference when designing program structure.

Diagram for state machine of our semaphore application:

 

Each state is able to transition to other state, which was defined earlier in the process. All transitions are conditioned, meaning in order for transition to happen, condition needs to be fulfilled.

So we will now describe each states possible transitions as also transition conditions.

It can transition to:

Run state
State transition condition: Start program

Estop state
State transition condition: Estop activated

Corresponding IDLE state parameter:

We can evaluate Idle state by combining three parameters.  Idle state is basically inverted run state, in a sense that no program or any other task is being executed. For this  we can use run state parameter !_hw_run. In order that we are 100% sure that system is not executing any remaining commands from the buffer, we also check _hw_buffempty parameter. We also need to be sure that Estop mode is not active with !_hw_estop.

(_hw_buffempty && !_hw_run) && !_hw_estop

 

Flowcharts:

 

#OnInit:

Below is a flowchart for #OnInit. #OnInit is a built in expression function, which is executed when TNG software is launched/initialized. It only has one iteration.  It is perfect for initialisation code of our semaphore.

At the beginning we define state decimal values and conditions for Alarm, Estop and Idle states.  Only these three states because, when system is powered up, it can only take up states Estop(button pressed), Alarm(alarm before system shutdown was not reset) or Idle(all is OK). So, depending on the conditions met, OnInit will set the state which will be then recognized in the #Loop function.

Snippet of code for flowchart above is following:

#OnInit
state_estop = 0; 
state_idle  = 1; 
state_pause = 2; 
state_run   = 3;  
state_alarm = 4;
from_init = 0;


cleared_not = !_alarm_cleared;
if(cleared_not,exec(state=state_alarm,from_init=1,msg('INIT: Please Reset Error')));

estop = _hw_estop && _hw_idle; 
if(estop, state=state_estop);

idle  = (_hw_buffempty && !_hw_run) && !_hw_estop && _alarm_cleared; 
if(idle, state=state_idle);

 

#Loop:

#Loop function’s role is that is constantly checks and recognizes active state of the system.

If we, for the sake of this tutorial say, that this will be #Loop’s first execution and iteration, and since we just launched the TNG system, somebody pressed the Estop button before system shutdown (many CNC machines actually recommend to activate Estop button before system shutdown). So, #OnInit recognized and set as state Estop state.

#Loop then checks for active state and executes corresponding procedure. In our case, Estop state -> #StateEstop

Snippet of code for flowchart above is following:

#Loop 
;Every 83ms
if (state == state_estop, exec('#StateEStop'));
if (state == state_idle, exec('#StateIdle'));
if (state == state_run, exec('#StateRun'));
if (state == state_pause, exec('#StatePause'));
if (state == state_alarm, exec('#StateAlarm'));

 

#StateEstop flowchart is below:

At the beginning we have two safety instances. First one checks if state is indeed StateEstop, second one checks if last valid state was not StateEstop. This is important since we execute the #StateEstopInit procedure only at first and initial state change.

#StateEstopInit procedure is where we actually set the new last state and activate corresponding light on the semaphore.

With _state_last = state_estop command we set new last state. _extout1|5 = 1  will activate Red light on our ExtInOut board. All other lights are in this state at 0.

 

Further we check for any state transition conditions being met. This will be continuously evaluated until Estop button is not released. So if Estop button is released and error has been reset, Idle state conditions are met and we execute procedure #StateEstopEnd and set new state, state_idle. And then we return back to #Loop, where Idle state is recognized and #StateIdle is executed.

Which state transitions conditions need to be checked within each state, is defined in our state machine at the beginning of this tutorial. 

 

Snippet of code for flowchart above is following:

#StateEStop
if (state != state_estop, return(0));
if (_state_last != state_estop, exec('#StateEStopInit'));

;Here we check for state transition conditions

idle = (_hw_buffempty && !_hw_run) && !_hw_estop; 
if(idle, exec('#StateEStopEnd', state = state_idle, return()));

if((!_alarm_cleared && !_hw_estop), exec('#StateEStopEnd', state = state_alarm, return()));

#StateEStopInit
_state_last = state_estop;
print('EStop: ',estop);
_extout1|5 = 1;
_extout1|1 = 0;
_extout1|2 = 0;
_extout1|3 = 0;

#StateEStopEnd

 

#StateRun flowchart is below:

At the beginning we have two safety instances. First one checks if state is indeed StateRun, second one checks if last valid state was not StateRun. This is important since we execute the #StateRunInit procedure only at first and initial state change.

#StateRunInit procedure is where we actually set the new last state and activate corresponding light on the semaphore.

With _state_last = state_run command we set new last state. _extout1|5 = 2  will activate Green light on our ExtInOut board. All other lights are in this state at 0.

 

Further we check for any state transition conditions being met. These conditions will be continuously evaluated until either Estop, Pause, Stop buttons are pressed or CNC doors are open.

Let’s say CNC doors are suddenly opened during program run (this is checked using the ExtInOut board’s input 1, _extin1|1).  Alarm state condition is met and program will execute procedure #StateRunEnd and set new state, state_alarm. Program returns back to #Loop function, where Alarm state is recognized and #StateAlarm procedure is executed.

Which state transitions conditions need to be checked within each state, is defined in our state machine at the beginning of this tutorial.