Cellular Modbus Client
Send requests and receive responses from Modbus servers via cellular.
You Will Need
- Visual Studio Code (VS Code) with the PlatformIO extension
- Notecarrier F
- Notecard
- Swan
- SparkFun Transceiver Breakout - RS-485
- SparkFun USB to RS-485 Converter
- 2 Screw Terminals 3.5mm Pitch (3-Pin)
- STLINK-V3MINI
- 2 micro USB cables
- 1 USB mini-B cable
- Male-to-male jumper wires
- Female-to-male jumper wires
- Soldering iron and solder flux
Hardware Setup
This application uses Modbus RTU with RS-485 for Modbus communication between the client and server. The RS-485 transceiver breakout is used by the client running on the Swan to send and receive Modbus messages. The USB to RS-485 converter is used by the server running on your development PC to send and receive Modbus messages.
RS-485 Transceiver Breakout
To use the RS-485 transceiver breakout board, solder on a row of 5 male headers and one of the 3-pin screw terminals:
Using a small flat head screwdriver, loosen the screws on the screw terminal and insert a male-to-male jumper wire into each port, tightening the screws after. Then, attach the female end of 5 female-to-male jumper wires to the male headers you just soldered on:
USB to RS-485 Converter
To use the USB to RS-485 converter, solder on the remaining 3-pin screw terminal:
Loosen the screws on the screw terminal and connect the loose ends of the male-to-male jumper wires from the RS-485 transceiver breakout to the corresponding ports of the converter's screw terminal, tightening the screws after. The pins are labeled on the backsides of both boards: B, A, and G.
Connect the converter to your development PC with the USB mini-B cable.
Notecarrier
- Assemble Notecard and Notecarrier as described in the Notecard Quickstart.
- Plug the Swan into the Notecarrier, aligning the Swan's male headers with the Notecarrier's female headers.
- Make the following connections between the RS-485 transceiver breakout and the Notecarrier using the loose ends of the male-to-male headers (the breakout pins are labeled on the backside of the board):
Breakout Notecarrier 3-5V F_3V3 RX-I F_A4 TX-O F_A5 RTS F_D5 GND GND - Use a male-to-male jumper wire to connect the ATTN and F_D13 pins of the Notecarrier.
- Connect one end of the JTAG ribbon cable that came with the STLINK to the STLINK and the other end to the Swan.
- Connect the STLINK to your development PC with a micro USB cable.
- Connect the Swan to your development PC with the remaining micro USB cable.
Notehub Setup
Sign up for a free account on notehub.io and create a new project.
Firmware
Operation
The firmware relies on the ArduinoRS485 and ArduinoModbus libraries to handle the details of the RS-485 and Modbus RTU protocols, respectively. At the application level, the firmware is driven by 2 Notefiles: requests.qi
and responses.qo
. The code uses the Notecard's ATTN pin to detect new requests added to requests.qi
. When it gets one, the firmware parses the request and sends out a corresponding Modbus frame over the Modbus. The firmware parses the response from the server and adds a response to responses.qo
. The following Modbus functions are supported:
Function | Function Code |
---|---|
Read Coils | 1 |
Read Discrete Inputs | 2 |
Read Holding Registers | 3 |
Read Input Registers | 4 |
Write Single Coil | 5 |
Write Single Register | 6 |
Write Multiple Coils | 15 |
Write Multiple Registers | 16 |
To call any of these functions, add a note with the proper structure to requests.qi
. The structure for each type is described in the following sections. There are a few fields common to all types:
server_addr
: The address of the Modbus server.seq_num
: A sequence number for the request. This number will be included in the response note inresponses.qo
so that requests can be matched to responses. The user should increment this value with every new request.func
: The Modbus function code.data
: A JSON object containing the function-specific data for the request. This object always contains anaddr
field, which specifies the start address of the coil(s)/register(s) to read/write.
Request Types
Read Coils
{
"server_addr": 1,
"seq_num": 0,
"func": 1,
"data": {
"addr": 0,
"num_bits": 16
}
}
num_bits
is the number of coils to read, starting from addr
.
Read Discrete Inputs
{
"server_addr": 1,
"seq_num": 1,
"func": 2,
"data": {
"addr": 0,
"num_bits": 16
}
}
This is identical to the request for read coils, except the function code is 2 instead of 1.
Read Holding Registers
{
"server_addr": 1,
"seq_num": 3,
"func": 3,
"data": {
"addr": 0,
"num_regs": 2
}
}
num_regs
is the number of 16-bit holding registers to read, starting from addr
.
Read Input Registers
{
"server_addr": 1,
"seq_num": 4,
"func": 4,
"data": {
"addr": 0,
"num_regs": 2
}
}
This is identical to the request for read holding registers, except the function code is 4 instead of 3.
Write Single Coil
{
"server_addr": 1,
"seq_num": 5,
"func": 5,
"data": {
"addr": 2,
"val": 1,
}
}
addr
is the address of the coil to write and val
is the value to write. Since coils are binary, val
must be 0 or 1.
Write Single Register
{
"server_addr": 1,
"seq_num": 6,
"func": 6,
"data": {
"addr": 4,
"val": 199
}
}
addr
is the address of the register to write and val
is the value to write. Since registers are 16-bits, val
must fit into 16 bits. Note that only holding registers are writable. Input registers are read only.
Write Multiple Coils
{
"server_addr": 1,
"seq_num": 7,
"func": 15,
"data": {
"addr": 0,
"num_bits": 16,
"coil_bytes": [99, 171]
}
}
addr
is the start address of the coils to write. num_bits
is the number of coils to write. coil_bytes
is an array of bytes containing the values to write. The least significant bit (LSB) of the first byte corresponds to the value to write at addr
, the next most significant bit corresponds to addr + 1
, and so on. The next byte's LSB corresponds to addr + 8
, the next most significant bit of that byte to addr + 9
, and so on. This table shows what values would be written to which addresses using the above example (in binary, 99 is 01100011 and 171 is 10101011):
Address | Value |
---|---|
addr | 1 |
addr + 1 | 1 |
addr + 2 | 0 |
addr + 3 | 0 |
addr + 4 | 0 |
addr + 5 | 1 |
addr + 6 | 1 |
addr + 7 | 0 |
addr + 8 | 1 |
addr + 9 | 1 |
addr + 10 | 0 |
addr + 11 | 1 |
addr + 12 | 0 |
addr + 13 | 1 |
addr + 14 | 0 |
addr + 15 | 1 |
If the number of bits to write (as indicated by num_bits
) isn't a multiple of 8, the last byte should be padded with 0s.
Write Multiple Registers
{
"server_addr": 1,
"seq_num": 8,
"func": 16,
"data": {
"addr": 2,
"vals": [4369, 8738]
}
}
addr
is the start address of the registers to write. vals
is an array of 16-bit values to write. The first value will be written to addr
, the second to addr + 1
, and so on. Again, only holding registers are writable.
Response Types
All responses have a seq_num
field, which is the sequence number corresponding to the request that produced the response.
Reads
On success, reading coils or discrete inputs has this response format:
{
"bits": [250, 112],
"seq_num": 0
}
bits
is a byte array packed in the same fashion as described in Write Multiple Coils.
Successful reading of registers has this response format:
{
"regs": [291, 43981],
"seq_num": 5
}
regs
is an array of 16-bit register values.
Writes
All successful write operations have the same response format:
{
"seq_num": 1
}
Errors
If any request results in an error, a response of this form will be added to responses.qo
:
{
"error": "Illegal data address",
"seq_num": 11
}
error
is a string describing the error. In this example, a bad address was supplied in the addr
field of the corresponding request.
Building and Flashing
To build and upload the firmware onto the MCU, you'll need VS Code with the PlatformIO extension.
- Download and install Visual Studio Code.
- Install the PlatformIO IDE extension via the Extensions menu of Visual Studio Code.
- Click the PlatformIO icon on the left side of VS Code, then click Pick a folder, and select the the firmware directory,
34-cellular-modbus-client/firmware
. - In the file explorer, open
main.cpp
and uncomment this line:// #define PRODUCT_UID "com.my-company.my-name:my-project"
. Replacecom.my-company.my-name:my-project
with the ProductUID of the Notehub project you created in Notehub Setup. - Click the PlatformIO icon again, and under the Project Tasks menu, click Build to build the firmware image.
- Under the Project Tasks menu, click Upload to upload the firmware image to the MCU.
From here, you can view logs from the firmware over serial with a terminal emulator (e.g. minicom, or pio device monitor
). You can determine the correct serial port by running pio device list
in a terminal window of VSCode. The command output lists all the serial devices and their logical names - look for the port corresponding to SWAN_R5 CDC in FS Mode
.
Testing
The tests described in this section rely on the server.py
script. This script uses pymodbus
to run a Modbus server on your development PC. It's based on pymodbus's datastore_simulator.py example. You can check out server.py
's config
dictionary to see how the coils and registers are addressed. Refer to pymodbus's documentation for details on how the memory layout of the server is configured.
Before proceeding with the sections below, install the Python dependencies with pip install -r requirements.txt
. You may want to do this inside a virtualenv to avoid polluting the system-level Python packages with these dependencies.
Simple Test
You'll use server.py
as the Modbus server and the Swan as the Modbus client.
- Run the server:
python server.py --log debug --port /dev/ttyUSB0
. The--port
parameter specifies the serial port of the USB to RS-485 converter. On Linux, this is typically/dev/ttyUSB0
, but you may need to alter this path depending on your machine. You should see output like this after starting the server:2023-06-06 12:31:05,050 INFO logging:96 Server(Serial) listening. 2023-06-06 12:31:05,050 INFO logging:96 Server(Serial) listening. 2023-06-06 12:31:05,050 DEBUG logging:102 Serial connection opened on port: /dev/ttyUSB0 2023-06-06 12:31:05,050 DEBUG logging:102 Serial connection opened on port: /dev/ttyUSB0 2023-06-06 12:31:05,050 DEBUG logging:102 Serial connection established 2023-06-06 12:31:05,050 DEBUG logging:102 Serial connection established
- Go to Notehub and open your project's Devices tab.
- Click the entry for your device and click the "+ Note" button.
- In the "Select notefile" box, input
requests.qi
. - In the "Note JSON" box, paste this request body:
{ "server_addr": 1, "seq_num": 0, "func": 15, "data": { "addr": 0, "num_bits": 16, "coil_bytes": [99, 171] } }
- Click "Add Note". This request will write the bits specified by
coil_bytes
into the coils at addresses 0-15. - Go back to your Notehub project and open the Events tab. You should see a
responses.qo
note like this:{ "seq_num": 0 }
- Now, add this note to
requests.qi
to read back the values you wrote in the first request:{ "server_addr": 1, "seq_num": 1, "func": 1, "data": { "addr": 0, "num_bits": 16 } }
- Go back to your Notehub project and open the Events tab. You should see a
responses.qo
note like this, with the values you wrote with the first request:{ "bits": [99, 171], "seq_num": 1 }
Test Script
To quickly exercise all the supported Modbus functions, you can run the test.py
Python script.
- Set up Programmatic API Access on your Notehub project by following this documentation. You now have a client ID and secret.
- Go to your Notehub project's Devices tab, double-click your device in the list, and copy down the Device UID.
- Go to your Notehub project's Settings tab and copy down the Project UID.
- You now have all the information you need to run
test.py
:python test.py \ --serial-port <USB to RS-485 converter serial port> \ --project-uid <Your Project UID> \ --device-uid <Your Device UID> \ --client-id <Your client ID> \ --client-secret <Your client secret>
The tests should take less than a minute to complete. They should all pass, and you should be able to see the requests and responses in your Notehub project's Events tab.
Troubleshooting
Should you encounter any errors, these are typically caused by missing wiring.
-
Connection timed out
/Invalid CRC
responses inresponses.qo
. This typically occurs when theD5
pin is not connected to theDE
andRE
pins on the RS485 breakout. The purpose of this pin is to control the direction of serial data to the RS485 breakout. Without it, data cannot be transmitted reliably. Double-check your wiring to ensure the jumper cable is in place betweenF_D5
andATTN
pins on the Notecarrier. -
Notes sent to
requests.qi
don't appear immediately, but do after a sync. The connection betweenF_D13
and theATTN
pin is used to notify the host (Swan) that a new note is available. If this connection is broken, the host isn't notified in a timely manner. Double-check your wiring to ensure the jumper cable is in place betweenF_D13
andATTN
pins on the Notecarrier. -
Notes sent to
request.qi
don't appear at all. This happens when the Notecard is not configured to the Notehub project. Ensure thatPRODUCT_UID
is set in the source code, or that you have issued ahub.set
request with theproduct
property set to your Notehub Project'sProductUID
.
Blues Community
We’d love to hear about you and your project on the Blues Community Forum!