LoRa remote water level and pump controller (ReWaL): client software – 3

Spread the love

For the client, a solar panel recharges a battery that powers the device, so It must go on sleep mode every time is possible and must notify the battery status to the server, and when It’s waking check the water lever sensor status and send It to the server, and must wait for an acknowledge to be sure the status is received, else resend the message, at the end send a message every time the status change.

LoRa wireless remote water tank and pump controller (esp8266) Client software Arduino IDE
LoRa wireless remote water tank and pump controller (esp8266) Client software Arduino IDE

EByte E32 configuration

The LoRa device is an E32, it is configured for a fixed transmission with a short cycle of WOR to be sure that it will intercept the long preamble of the server so that it can reactivate.

Refer to the article “Ebyte LoRa E32 device for Arduino, esp32 or esp8266: configuration“.

		ResponseStructContainer c;
		c = e32ttl.getConfiguration();
		Configuration configuration = *(Configuration*) c.data;
		configuration.ADDL = CLIENT_ADDL;
		configuration.ADDH = CLIENT_ADDH;
		configuration.CHAN = CLIENT_CHANNEL;
		configuration.OPTION.fixedTransmission = FT_FIXED_TRANSMISSION;
		configuration.OPTION.wirelessWakeupTime = WAKE_UP_250;

		configuration.OPTION.fec = FEC_1_ON;
		configuration.OPTION.ioDriveMode = IO_D_MODE_PUSH_PULLS_PULL_UPS;
		configuration.OPTION.transmissionPower = POWER_20;

		configuration.SPED.airDataRate = AIR_DATA_RATE_010_24;
		configuration.SPED.uartBaudRate = UART_BPS_9600;
		configuration.SPED.uartParity = MODE_00_8N1;

		ResponseStatus rs = e32ttl.setConfiguration(configuration, WRITE_CFG_PWR_DWN_SAVE);

		c = e32ttl.getConfiguration();
		Configuration configurationNew = *(Configuration*) c.data;


The microcontroller immediately goes into soft sleep mode and listens on the AUX pin for activation.

The management of sleep mode is explained on “Ebyte LoRa E32 device for Arduino, esp32 or esp8266: WOR (wake on radio) microcontroller and new WeMos D1 mini shield”.

		gpio_pin_wakeup_enable(GPIO_ID_PIN(AUX_PIN), GPIO_PIN_INTR_LOLEVEL);

So when a message arrives the WeMos D1 wakes up and starts to retrieve the message.

Client operation

Immediately upon awakening, the minimum and maximum level from the sensor will be set

void setMin(){
	uint8_t valMin = digitalRead(TANK_MIN);
	minLevel = valMin==LOW;
void setMax(){
	uint8_t valMax = digitalRead(TANK_MAX);
	maxLevel = valMax==LOW;

if minLevel is true means that the tank is empty, if maxLevel is true means that the tank is full.

After the wake, the operation checks if there are data on the E32 buffer, and process all the message

	if (e32ttl.available()){
		SERIAL_DEBUG.println("Start reading!");

		ResponseContainer rs = e32ttl.receiveMessage();
		String message = rs.data;


		deserializeJson(doc, message);

		String type = doc["type"];
		SERIAL_DEBUG.print("type --> ");

		operationalSelected = static_cast<OPERATION_MODE>((int)doc["mode"]);

		ResponseStatus rsW;

		if (type=="start"){
			pumpIsActive = true;

			SERIAL_DEBUG.println("Operation complete!!");

			if (batteryLevel>1){
				batteryTimePassed = 0;
				btSended = false;

		}else if(type=="stopp"){
			batteryTimePassed = 0;

			pumpIsActive = false;
			ResponseStatus rsUpdate = sendUpdate(PACKET_PUMP_LEVEL);
			rsW = setModeSleep();
			SERIAL_DEBUG.println("Operation complete, go to sleep!!");
		}else if (type=="ackpa"){
			needAck = false;

		ResponseStatus rsUpdate = sendUpdate(PACKET_PUMP_LEVEL);

		SERIAL_DEBUG.println("Update complete!!");

		timePassed = millis();


Here is a simple communication schema

Water tank level and pump controller LoRa messages
Water tank level and pump controller LoRa messages

Server messages

The basic type of messages that can be received from the server is:

  • start: we want to start the pump and need the updated level of the tank in response;
  • stop: the pump is stopped and the client must go to sleep;
  • ackpa: confirmation of receipt message.

Start message

The start message informs us that must start the pump, and to start operating the server need the Tank level.

The server sends the OPERATION_MODE also, that basically says to the client that if It’s

  • OPERATION_NORMAL: no ping needed and the Server waits for only the level status when change;
  • OPERATION_PING: after the reading of the message, the client starts to send a ping with tank level to the server, if the server does not receive the ping after 20 seconds It stops the pump and gives an error.

Stop message

The stop message simply notifies that we stop the pump, so the client sends the last information message and go to sleep.

Acknowledge message

This is the receiving confirmation message if no acknowledgment arrives the client resends the status message.

Client message

There are 2 simple types of client message, and these are managed with this function

ResponseStatus sendUpdate(PACKET_TYPE packetType, bool needAckParam ){
	SERIAL_DEBUG.println(" ------------ START ---------------");

	JsonObject root = doc.to<JsonObject>(); // get the root object

	switch (packetType) {
			batteryLevel = getBatteryVoltage();
			SERIAL_DEBUG.print(F(" BATTERY --> "));
			root["ty"] = "bl";
			root["pn"] = packetNumber++;
			root["battLev"] = batteryLevel;

			root["ty"] = "ppl";
			root["pn"] = packetNumber++;
			root["maxL"] = (maxLevel?1:0);
			root["minL"] = (minLevel?1:0);
			root["ack"] = needAckParam?1:0;

	int size = measureJson(doc)+1;

	char buf[size];
	serializeJson(doc, buf, size);

	SERIAL_DEBUG.print("Send message to server ");
	SERIAL_DEBUG.print(" ");
	SERIAL_DEBUG.print(" ");
	SERIAL_DEBUG.println(" ");

	SERIAL_DEBUG.println("Check mode ");

	ResponseStatus rsW = setModeNormal();


	if (rsW.code!=SUCCESS) return rsW;

	ResponseStatus rsSend = e32ttl.sendFixedMessage(SERVER_ADDH, SERVER_ADDL, SERVER_CHANNEL, buf, size);

	if (rsSend.code==SUCCESS && needAckParam){
		ackStartTime = millis();
		needAck = true;

	return rsSend;

The messages are:

  • BATTERY_LEVEL: that sends the level of the battery in volts;
  • PACKET_PUMP_LEVEL: that sends the min and max level status.

Battery level

This is a message to manage the battery level, I get the value of the analog pin with a voltage divider and I convert the value into volts.

For the management of the battery, you can refer to the article “Voltage divider: calculator and application“.

float getBatteryVoltage(){
	//************ Measuring Battery Voltage ***********
	float sample1 = 0;

	for (int i = 0; i < 100; i++) {
		sample1 = sample1 + analogRead(A0); //read the voltage from the divider circuit
	sample1 = sample1 / 100;
	float batVolt = (sample1 * 3.3 * (BAT_RES_VALUE_VCC + BAT_RES_VALUE_GND) / BAT_RES_VALUE_GND) / 1023;

	int bvI = batVolt * 100;
	batVolt = (float)bvI/100;
	return batVolt;

The update interval of the update is bigger when the battery is charged, and decreases when the battery becomes discharged.

The packet with the water level

Here we send the value of the min level switch and max level switch with the request of acknowledging, this message is sent when the pump starts and when the interrupts of level switches are fired.

	  pinMode(TANK_MIN, INPUT);
	  pinMode(TANK_MAX, INPUT);

	  attachInterrupt(digitalPinToInterrupt(TANK_MIN), minCallack, CHANGE );
	  attachInterrupt(digitalPinToInterrupt(TANK_MAX), maxCallack, CHANGE );


You can find the complete client code on my GitHub as usual.

  1. LoRa remote water tank level and pump controller: intro
  2. LoRa remote water level and pump controller: server software
  3. LoRa remote water level and pump controller: client software
  4. LoRa remote water level and pump controller: server PCB
  5. LoRa remote water level and pump controller: client PCB
  6. LoRa remote water level and pump controller: assemble server and 3D printed case
  7. LoRa remote water level and pump controller: assemble client and 3D printed case

Spread the love

Leave a Reply

Your email address will not be published. Required fields are marked *