Sidebar

Home



Knowledge Base


Guides & Tutorials

Projects

Samples

kb:tutorials:expert:use_expressions_and_modbus_to_command_relay_boards

Use Expressions and MODBUS to command relay boards

This tutorial 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.

Demonstration 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 array 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.

Serial expression functions

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.


Demonstration example

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:

Expr.txt file

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.

Final design of dedicated Expr.txt file

I 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:

#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 arguments 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>
;M18 code:
;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:

kb/tutorials/expert/use_expressions_and_modbus_to_command_relay_boards.txt · Last modified: 2023/08/03 12:22 by planetcnc

Page Tools