Monitoring and adjusting tank levels, in-situ and remotely, are everyday tasks operated in many industries, even at home. Some industrial applications include transport and storage tanks like, for example, a tank in a water treatment plant. In household applications, tank level monitoring is essential for applications as water dispensers, water evaporators, streamers, boiler monitoring systems, heating systems, washing machines, steam irons, automated coffee machines, and so on. With its industrial IoT capabilities, Opta™ micro PLC can be the perfect solution for these industrial applications.
Present application note aims to show a system capable of monitoring and adjusting two tanks' level using Opta™. We will refer to these tanks as Big Tank (BT) and Small Tank (ST). The application goals are the following:
A graphical representation of the intended application is shown below:
The Big Tank has at least twice the capacity of the Small Tank in the experimental setup shown above. The Opta™ devices communicate with each other using Modbus RTU protocol over the RS-485 interface to oversee its responsible tank.
ArduinoRS485
, ArduinoModbus
, and Scheduler
. You can install these libraries via the Library Manager of the Arduino IDE.The electrical connections of the intended application are shown in the diagram below:
The two Opta™ devices will communicate with each other using the Modbus RTU protocol. The level sensors (vertical and horizontal float switches) are monitored via the digital input pins of each Opta™; the pump and the solenoid/ball valve are controlled using the built-in relay outputs of both the Opta™ PLCs.
Each tank has a specific monitoring routine to track and control their minimum and maximum levels. Both Opta™ devices will exchange important state information and parameters to understand and take appropriate actions to maintain the desired capacities in the application. As mentioned before, the Opta™ devices in charge of managing the Small Tank (ST) and Big Tank (BT) will communicate with each other using the Modbus RTU protocol. The Opta™ managing the Big Tank will be the Client, while the one in charge of managing the Small Tank will behave as Server.
The Opta™ responsible of the Big Tank (BT) performs the following actions:
The Opta™ responsible of the Small Tank (ST) performs the following actions:
In addition to these responsibilities, both Opta™ devices are connected to the Arduino Cloud via Wi-Fi® to leverage monitored functionalities and remote control.
Hereafter we will highlight some sections of the code, crucial for the operation of the Opta™ in charge of the Small Tank management. Please note that some functions in the code are generated by the Arduino Cloud during the dashboard configuration. We will begin with the required libraries.
The following headers are required to enable the Modbus RTU protocol, the connection with the Arduino Cloud, and the scheduler. The scheduler will prioritize to handle the data exchange using Modbus RTU protocol while the local tasks are under
loop()
function of the Small Tank. Here are also defined the parameters needed per Modbus RTU specification as preDelay
and postDelay
.1#include "thingProperties.h"2#include "stm32h7xx_ll_gpio.h"3#include <Scheduler.h>4
5#include <ArduinoRS485.h>6#include <ArduinoModbus.h>7
8// Calculate preDelay and postDelay in microseconds as per Modbus RTU Specification9// MODBUS over serial line specification and implementation guide V1.0210// Paragraph 2.5.1.1 MODBUS Message RTU Framing11// https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf12constexpr auto baudrate { 19200 };13constexpr auto bitduration { 1.f / baudrate };14constexpr auto preDelayBR { bitduration * 9.6f * 3.5f * 1e6 };15constexpr auto postDelayBR { bitduration * 9.6f * 3.5f * 1e6 };
The vertical and horizontal float switches are essential sensors for recognizing the tank's capacity. It will measure if the switches are closed or open by reading the voltage in this scenario. For example, if the vertical switch is closed and indicates the tank is at maximum capacity, it will read ~3.0V and return its state as 1. Otherwise, it will return the maximum capacity state as 0, meaning it has not reached the upper capacity limit.
Usually, a two-state element would suffice to determine such state conditions, but it may be better if also unforeseen states are kept observable. The simple
analogRead()
method is used to convert the raw value reading into comprehensible information by using analogRead() * (3.249 / 4095.0) / 0.3034;
to represent in range of 0-3.2V in the current example. The ~3.0V will mean that the sensor is closed, while 0V will mean it is open.However, reading values between 1.8 and 2.4V for an extended period during the operation could mean that the sensor has lost its calibration or is experiencing a failure. This reading could be helpful to either check, replace, or fix the sensor that is giving uncertain figures.
1/**2 Checks for Small Tank's minimum and maximum sensor state.3
4 @param ST_Max Small Tank's maximum sensor state.5 @param ST_Min Small Tank's minimum sensor state.6*/7uint8_t ST_Level_Check(){8 // Simple sensor read state 9 ST_Max = ST_MaxSensor_A0();10 ST_Min = ST_MinSensor_A1();11}12
13...14
15/**16 Analog reading from A0 for Small Tank's maximum sensor (Vertical float switch).17
18 @param ST_Max_Cloud Small Tank's maximum sensor on Cloud side.19 @return Returns 1 or 0 depending on the converted analog read and ST_Max_Cloud.20*/21uint8_t ST_MaxSensor_A0(){22 digitalWrite(LEDB, HIGH);23 int st_max_read = analogRead(A0);24 float st_max_read_V = st_max_read * (3.249 / 4095.0) / 0.3034;25
26 Serial.print(F("Small Tank - Max = "));27 Serial.println(st_max_read_V, 3);28
29 digitalWrite(LEDB, LOW);30
31 if (st_max_read_V >= 2.99){32 ST_Max_Cloud = true;33 return 1;34 } else {35 ST_Max_Cloud = false;36 return 0;37 }38}39
40/**41 Analog reading from A1 for Small Tank's minimum sensor (Horizontal float switch).42
43 @param ST_Min_Cloud Small Tank's minimum sensor on Cloud side.44 @return Returns 1 or 0 depending on the converted analog read and ST_Min_Cloud.45*/46uint8_t ST_MinSensor_A1(){47 digitalWrite(LEDB, HIGH);48 int st_min_read = analogRead(A1);49 float st_min_read_V = st_min_read * (3.249 / 4095.0) / 0.3034;50
51 Serial.print(F("Small Tank - Min = "));52 Serial.println(st_min_read_V, 3);53
54 digitalWrite(LEDB, LOW);55
56 if (st_min_read_V >= 2.99){57 ST_Min_Cloud = true;58 return 1;59 } else {60 ST_Min_Cloud = false;61 return 0;62 }63}
The Opta™ monitoring the Small Tank will need to recognize the reservoir's capacity and use such information to maintain its nominal volume. A 2/2-way normally closed direct acting solenoid or a motorized ball valve is used in this setup and controlled by the Small Tank's Opta™ manager, to free the volume whenever certain conditions are applicable.
The following function helps to control the valve by reading the reservoir's capacity and external information from the Big Tank. The
BT_Min
is the float switch state for Big Tank's minimum level, obtained via Modbus RTU communication from the Opta™ managing the Big Tank.1/**2 Monitors the Small Tank's valve depending on the compilation of the sensors' states, and send the pump OFF command.3
4 @param ST_Valve Small Tank's valve state.5 @param ST_Valve_Cloud Small Tank's valve state on Cloud side.6 @param ST_Min Small Tank's minimum sensor state.7 @param ST_Max Small Tank's maximum sensor state.8*/9uint8_t ST_Volume_CTRL(){10 // Active main condition to free Small Tank volume11 if (((ST_Min == 0 && BT_Min == 1) && ST_Max != 1)){12 if (ST_Valve != 1){13 ST_Valve = 1;14 ST_Valve_Cloud = true;15 digitalWrite(D2, ST_Valve);16 17 Serial.println(F("Small Tank - Valve Opening"));18 } else {19 Serial.println(F("Small Tank - Valve Opened"));20 } 21 }22
23 // Conditional to halt volume freeing process for Small Tank24 if (ST_Max == 1 || BT_Min == 0){25 if (ST_Valve != 0){26 ST_Valve = 0;27 ST_Valve_Cloud = false;28 digitalWrite(D2, ST_Valve);29 Serial.println(F("Small Tank - Valve Closing"));30 } else {31 Serial.println(F("Small Tank - Valve Closed"));32 }33 // Sending Big Tank Pump Off Command34 ModbusRTUServer.inputRegisterWrite(0, 0x50);35 delay(40);36 }37}
As the Opta™ receives
BT_Min
from the Big Tank, the Small Tank also shares the information with the Big Tank regarding Small Tank's maximum level tagged as ST_Max
.1/**2 Shares Small Tank's parameters with Big Tank based on the Small Tank's maximum sensor state.3
4 @param ST_Max Small Tank's maximum sensor state.5*/6void ST_Param_Share(){7 // Simple representation for Small Tank's Maximum level sensor8 // 6 for ST_Max = 19 // 7 for ST_Max = 010 if (ST_Max == 1){11 ModbusRTUServer.inputRegisterWrite(0, 0x36);12 Serial.println(F("Small Tank - Maximum Level: ON"));13 } else {14 ModbusRTUServer.inputRegisterWrite(0, 0x37);15 Serial.println(F("Small Tank - Maximum Level: OFF"));16 }17 delay(100);18}
The following method is used whenever an Opta™ exchanges information with another Opta™ using Modbus RTU protocol. This method writes the input register values for addresses Opta™ defined through the specified Modbus address.
1ModbusRTUServer.inputRegisterWrite(0, 0x37)
In the meantime, the Big Tank's Opta™ will send such parameters, while the Small Tank Opta™ will poll for Modbus RTU requests to determine whether to activate a specific module or know if it is activated. In this example, if we receive
0x56
from the Big Tank Opta™, the Small Tank will turn off the valve. If it captures the data 0x31
or 0x32
, the Small Tank will have the information regarding Big Tank's minimum level state. The following simple parser does this task inside the Small Tank's Opta™.1/**2 Sets system parameter states depending on the Modbus RTU requests poll. 3
4 @param bigTank_coil Input Register value reading from Big Tank.5 @param ST_Valve Small Tank's valve state.6 @param BT_Min Big Tank's minimum sensor state.7*/8uint8_t RTU_parser(){9 // poll for Modbus RTU requests10 ModbusRTUServer.poll();11
12 long bigTank_coil = ModbusRTUServer.holdingRegisterRead(0);13
14 if (bigTank_coil == 0x56){15 Serial.println(F("Received: Big Tank - Valve Off"));16 ST_Valve = 0;17 digitalWrite(D2, ST_Valve);18 }19 if (bigTank_coil == 0x31){20 Serial.println(F("Received: Big Tank - Min - On"));21 BT_Min = 1;22 }23 if (bigTank_coil == 0x32){24 Serial.println(F("Received: Big Tank - Min - Off"));25 BT_Min = 0;26 }27 Serial.println(F(""));28 delay(40);29}
The setup process to enable all the needed features to manage Small Tank's Opta™ can be found below. The Modbus RTU protocol, scheduler, Arduino Cloud, and other features are configured and enabled here.
1/**2 Sets up Modbus RTU protocol configuration.3*/4void RTU_Setup(){5 Serial.println(F("Small Tank - Modbus RTU Client"));6
7 RS485.setDelays(preDelayBR, postDelayBR);8
9 if (!ModbusRTUServer.begin(42, baudrate, SERIAL_8E1)) {10 Serial.println("Failed to start Modbus RTU Server!");11 while (1);12 }13
14 // configure holding registers & input registers at address 0x0015 ModbusRTUServer.configureHoldingRegisters(0x00, 5);16 ModbusRTUServer.configureInputRegisters(0x00, 5);17}18
19void setup() {20 // Initial parameter initialization21 EM_Stop_ST = false;22 ST_Valve_Cloud = false;23
24 Serial.begin(9600);25 while (!Serial);26
27 delay(1000);28 29 // Analog/Digital IO Port Configuration30 analogIO_Setup();31 digitalIO_Setup();32
33 // Modbus RTU Configuration 34 RTU_Setup();35 36 // Status LED configuration;37 finder_led_Setup();38 digitalWrite(LEDG, HIGH);39 40 // Scheduler -> Modbus RTU41 Scheduler.startLoop(modbus_line);42 43 // This delay gives the chance to wait for a Serial Monitor without blocking if none is found44 delay(1500); 45
46 // Defined in thingProperties.h47 initProperties();48
49 // Connect to Arduino IoT Cloud50 ArduinoCloud.begin(ArduinoIoTPreferredConnection);51 52 /*53 The following function allows you to obtain more information54 related to the state of the network, the IoT Cloud connection and the errors55 the higher is the number, the more granular will be the information you’ll get.56 The default is 0 (only errors).57 Maximum is 458 */59 setDebugMessageLevel(2);60 ArduinoCloud.printDebugInfo();61}
The main
loop()
manages the overall tank's processes and its local parameters. The modbus_line()
function handles the data exchange between the two Opta™ devices using the Modbus RTU protocol.1void loop() {2 ArduinoCloud.update();3 4 if (EM_Stop_ST == false){5 // Essential tank runtime and parameter display6 ST_Level_Check();7 ST_Param_Monitor();8 9 // Small Tank Condition Checkers10 component_state();11 ST_Volume_CTRL();12 } else {13 Serial.println(F("Small Tank - Emergency Stop - Cloud"));14 ST_Valve = 0;15 ST_Valve_Cloud = false;16 }17 delay(1000);18}19
20/**21 Dedicated function for scheduler on handling ST_Param_Share() and RTU_parser().22*/23void modbus_line(){24 ST_Param_Share();25
26 // Modbus RTU w/ Big Tank PLC27 RTU_parser();28 delay(100);29}
The Opta™ in charge of the management of the Big Tank has a similar structure to the Small Tank's Opta™, such as the Arduino Cloud code generated during the configuration. We will focus on the primary responsibilities of the Opta™ managing the Big Tank and configured as a Client.
The Big Tank Opta™ code has two main tasks: to halt the system's operation due to an emergency stop state and to control the attached pump. The
BT_System_Off()
is triggered if the minimum level flag is false, which will halt the pump and send the valve OFF command for the Opta™ in charge of the Small Tank. Thus, the system emergency stop is prompted. The BT_Pump_CTRL()
will send the valve OFF request whenever the Big Tank's capacity reaches the maximum level and activate the pump to avoid the reservoir's overfill.1/**2 Monitors Big Tank's system to trigger emergency stop when minimum sensor is false, and closes the Small Tank's valve.3
4 @param BT_Pump Big Tank's pump state.5 @param BT_Min Big Tank's minimum sensor state.6 @param Sys_EM_Stop Big Tank's emergency stop state.7*/8uint8_t BT_System_Off(){9 if (BT_Min != 1){10 // Sending Small Tank Valve Off Command11 writeHoldingRegisterValues(42, 0x00, 0x56, 1);12 delay(40);13
14 // Turn off 15 BT_Pump = 0;16 Sys_EM_Stop = true;17
18 digitalWrite(D2, BT_Pump);19 Serial.println(F("Big Tank - Level Below Nominal: Emergency Stop"));20 } else {21 Sys_EM_Stop = false;22 Serial.println(F("Big Tank - Level Above Nominal"));23 }24}25
26/**27 Monitors Big Tank's pump depending on maximum sensor state, and closes the Small Tank's valve.28
29 @param BT_Pump Big Tank's pump state.30 @param BT_Pump_Cloud Big Tank's pump state on Cloud side.31 @param BT_Max Big Tank's maximum sensor state.32*/33uint8_t BT_Pump_CTRL(){34 if (BT_Max != 0){35 // Sending Small Tank Valve Off Command36 writeHoldingRegisterValues(42, 0x00, 0x56, 1);37 delay(40);38
39 // Turn on the BT Pump40 if (BT_Pump != 1){41 BT_Pump = 1;42 BT_Pump_Cloud = true;43 digitalWrite(D2, BT_Pump);44 Serial.println(F("Big Tank - Pump Initiating"));45 } else {46 Serial.println(F("Big Tank - Pump Active"));47 }48 } else if (BT_Max == 0 || ST_Max == 1){49 if (BT_Pump != 0){50 BT_Pump = 0;51 BT_Pump_Cloud = false;52 digitalWrite(D2, BT_Pump);53 Serial.println(F("Big Tank - Pump Stopping"));54 } else {55 Serial.println(F("Big Tank - Pump Inactive"));56 }57 }58}
The Opta™ in charge of the Big Tank shares the information regarding the Big Tank's minimum level, tagged as
BT_Min
inside the sketch, with the Small Tank.1/**2 Shares Big Tank's parameters with Small Tank based on the Big Tank's minimum sensor state.3
4 @param BT_Min Big Tank's minimum sensor state.5*/6void BT_Param_Share(){7 // Simple representation for Big Tank's Minimum level sensor8 // 0x31 for BT_Min = 19 // 0x32 for BT_Min = 010 if (BT_Min == 1){11 writeHoldingRegisterValues(42, 0x00, 0x31, 1);12 Serial.println(F("Big Tank - Minimum Level: ON"));13 } else {14 writeHoldingRegisterValues(42, 0x00, 0x32, 1);15 Serial.println(F("Big Tank - Minimum Level: OFF"));16 }17 delay(100);18}
In this example, if we receive
0x50
from the Opta™ managing the Small Tank, the Big Tank will turn off the pump. If it captures 0x36
or 0x37
, the Big Tank will have the information regarding Small Tank's maximum level. The following simple parser does this task inside the Big Tank's Opta™. The minor difference between the Small Tank Opta™ resides in how it seeks for the data to retrieve. The Big Tank Opta™ will use readInputRegisterValues(42, 0x00, 1)
to request for data from the Opta™ managing the Small Tank, if available.1/**2 Sets system parameter states depending on the received Input Register value. 3
4 @param smallTank_coil Input Register value reading from Small Tank.5 @param BT_Pump Big Tank's pump state.6 @param ST_Max Small Tank's maximum sensor state.7*/8uint8_t RTU_parser(){9 smallTank_coil = readInputRegisterValues(42, 0x00, 1);10
11 if (smallTank_coil == 0x50){12 Serial.println(F("Received: Small Tank - Pump Off"));13 BT_Pump = 0;14 digitalWrite(D2, BT_Pump);15 }16 if (smallTank_coil == 0x36){17 Serial.println(F("Received: Small Tank - Maximum Level - On"));18 ST_Max = 1;19 }20 if (smallTank_coil == 0x37){21 Serial.println(F("Received: Small Tank - Maximum Level - Off"));22 ST_Max = 0;23 }24 Serial.println(F(""));25 delay(40);26}
Since the Opta™ in charge of the Big Tank is the Client, the Modbus RTU protocol is configured accordingly with the Client's properties. The commonly used method in this example for the Client will be
writeHoldingRegisterValues()
and readInputRegisterValues()
. The first method will write values to the Small Tank Opta™ when certain conditions are flagged, while the second method will request information to track parameters from the Opta™ in charge of Small Tank.1/**2 Sets up Modbus RTU protocol configuration.3*/4void RTU_Setup(){5 Serial.println(F("Big Tank - Modbus RTU Client"));6
7 RS485.setDelays(preDelayBR, postDelayBR);8
9 // start the Modbus RTU client10 if (!ModbusRTUClient.begin(baudrate, SERIAL_8E1)) {11 Serial.println("Failed to start Modbus RTU Client!");12 while (1);13 }14}15
16/**17 Writes Holding Register values given argument inputs. 18
19 @param dev_address Device address.20 @param reg_address Register address.21 @param holding_write Data to write.22 @param byte_count Number of bytes.23*/24void writeHoldingRegisterValues(int dev_address, uint8_t reg_address, uint8_t holding_write, int byte_count){25 ModbusRTUClient.beginTransmission(dev_address, HOLDING_REGISTERS, reg_address, byte_count);26 ModbusRTUClient.write(holding_write);27
28 if (!ModbusRTUClient.endTransmission()) {29 Serial.print(F("Holding Register Write - Failed! "));30 Serial.println(ModbusRTUClient.lastError());31 } else {32 Serial.println(F("Holding Register Write - Success"));33 }34}35
36/**37 Reads Holding Register values given argument inputs. 38
39 @param dev_address Device address.40 @param reg_address Register address.41 @param byte_count Number of bytes.42 @param packet Holding register value reading.43*/44void readHoldingRegisterValues(int dev_address, uint8_t reg_address, int byte_count, uint8_t packet){45 if (!ModbusRTUClient.requestFrom(dev_address, HOLDING_REGISTERS, reg_address, byte_count)) {46 Serial.print(F("Holding Register Read - Failed! "));47 Serial.println(ModbusRTUClient.lastError());48 } else {49 Serial.println(F("Holding Register Read - Success"));50
51 while (ModbusRTUClient.available()) {52 Serial.print(ModbusRTUClient.read());53 packet = ModbusRTUClient.read();54 Serial.print(' ');55 }56 Serial.println();57 }58}59
60/**61 Reads Input Register values given argument inputs. 62
63 @param dev_address Device address.64 @param reg_address Register address.65 @param byte_count Number of bytes.66*/67uint8_t readInputRegisterValues(int dev_address, uint8_t reg_address, int byte_count){68 uint8_t packet;69 if (!ModbusRTUClient.requestFrom(dev_address, INPUT_REGISTERS, reg_address, byte_count)) {70 Serial.print(F("Input Register Read - Failed! "));71 Serial.println(ModbusRTUClient.lastError());72 return 0;73 } else {74 Serial.println(F("Input Register Read - Success"));75
76 while (ModbusRTUClient.available()) {77 packet = ModbusRTUClient.read();78 }79 return packet;80 }81}
In the loop and assigned scheduler function, the Opta™ managing the Big Tank will share its local parameters with the Small Tank's Opta™, consistently checking for the pump's activation or if the system must activate an emergency stop.
1void loop() {2 ArduinoCloud.update();3
4 if (EM_Stop_BT == false){5 // Essential tank runtime and parameter display6 BT_Level_Check();7 BT_Param_Monitor();8 9 // Big Tank Condition Checkers10 component_state();11 BT_System_Off();12 BT_Pump_CTRL();13 } else {14 Serial.println(F("Big Tank - Emergency Stop Triggered"));15 BT_Pump = 0;16 BT_Pump_Cloud = false;17 }18 delay(1000);19}20
21/**22 Dedicated function for scheduler on handling BT_Param_Share() and RTU_parser().23*/24void modbus_line(){25 BT_Param_Share();26
27 // Modbus RTU w/ Small Tank PLC28 RTU_parser();29 delay(100);30}
Thanks to the Arduino Cloud, we can create a simple but useful dashboard to have a professional real-time Human-Computer Interaction (HCI) as it can be seen below:
Within Arduino Cloud's dashboard, the system status of both the tanks can be monitored and the remote actuation is implemented for both the Opta™ devices' managed tasks. Using this powerful tool, the actuators and emergency stop can be controlled remotely on-demand. The dashboard can also be used to make a simulation, even without the full exact hardware implementation of the application note.
The complete code for the Small and Big Tank's management implementation with Opta™ can be downloaded here. It is important to know that for both,
thingProperties.h
is included with its respective variables and is generated automatically with Arduino Cloud.In this application note, we have learned how to set up the communication between two Opta™ devices using the Modbus RTU protocol, to exchange data, and to build a simple tank-level monitoring system using its I/O ports. We have also learned how to use the Arduino Cloud features to have an on-demand trigger and to monitor the actual tank-level information through a dashboard that displays statistics of the whole system in real-time.
Now that you have learned how to design and build a tank level monitoring system with Opta™, using the Modbus RTU protocol and the Arduino Cloud platform for on demand remote actuation, you can explore the possibilities to scale your projects further, by integrating Opta™ as a part of a manufacturing or maintenance system.