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: