GITHUB REPOSITORY : EC25_MQTT_NORVI_GSM_SERIES #
NORVI devices are robust industrial controllers designed for IoT applications. When combined with the Quectel EC25 module, these devices can leverage cellular connectivity to communicate with cloud platforms securely using the MQTT protocol.
This guide provides an example implementation on how to use NORVI devices with the Quectel EC25 module to establish a connection with an MQTT broker using AT commands. The Quectel EC25 is a popular LTE module widely used in IoT applications for cellular communication. This guide will cover the necessary setup, code explanation, and essential AT commands.
MQTT PROTOCOL #
MQTT (Message Queuing Telemetry Transport) is a lightweight messaging protocol designed for low-bandwidth, high-latency, or unreliable networks. It’s widely used in Internet of Things (IoT) and machine-to-machine (M2M) communication due to its simplicity and efficiency.
Check this link to understand more about the MQTT PROTOCOL.
Here are the components of MQTT,
Publisher-Subscriber Model #
- MQTT follows a publisher-subscriber model. In this model, there are two main entities, publishers and subscribers.
- Publishers are devices or applications that publish messages on a specific topic.
- Subscribers are devices or applications that receive messages by subscribing to specific topics.
Broker #
- MQTT communication is facilitated by a broker. The broker is a server responsible for receiving, routing, and delivering messages.
- Publishers and subscribers connect to the broker to send and receive messages.
- The broker is responsible for managing topics, subscribers, and message delivery.
Topics #
- Messages in MQTT are published to topics. Topics are hierarchical and can be thought of as addresses.
- Subscribers can subscribe to entire topics or specific subtopics using wildcards.
- For example, a topic could be sensors/temperature where sensors are the main topic and temperature is a subtopic.
Retained Messages #
- MQTT allows for retained messages, which are special messages that are stored by the broker and sent to new subscribers immediately upon subscription.
- This feature is useful for sending status updates or configuration information to new subscribers.
Quality of Service (QoS) #
- MQTT supports different levels of Quality of Service (QoS) for message delivery:
- QoS 0 (At most once): Messages are delivered at most once, without confirmation.
- QoS 1 (At least once): Messages are guaranteed to be delivered at least once, but duplicates may occur.
- QoS 2 (Exactly once): Messages are guaranteed to be delivered exactly once.
Understanding NORVI Devices with the Quectel EC25 Module #
NORVI devices are known for their reliability in industrial environments, featuring various input/output options, robust enclosures, and compatibility with multiple communication protocols. The integration of the Quectel EC25 module allows these devices to operate in areas where traditional Wi-Fi or Ethernet connections are not feasible. The Quectel EC25 module provides LTE connectivity, enabling NORVI devices to transmit data securely over cellular networks.
Setting Up NORVI GSM Series EC25 to MQTT #
Here we’ll be considering the NORVI GSM series EC25 device as a Publisher and “MQTT.FX” software as the Subscriber. With the NORVI device, we’ll be storing the I/O values in the MQTT Subscriber and the data visualization platform.
Key features of NORVI devices with the Quectel EC25 module,
- Industrial-grade design: Suitable for harsh environments.
- Versatile I/O options: Digital and analog inputs/outputs for various sensors and actuators.
- Cellular connectivity: LTE support via the Quectel EC25 module for remote monitoring and control.
Prerequisites
- Before you begin, ensure you have the following,
- NORVI Device with an integrated Quectel EC25 module
- Registered Sim card
- MQTT broker details (hostname, port, username, password)
- MQTT.fx software for testing and monitoring
- Arduino IDE and necessary libraries for programming the NORVI device.
- Data visualization platform (e.g., DATACAKE)
Required Libraries
- Arduino.h
- Wire.h
- WiFi.h
- ArduinoJson.h
Sensitive Information Handling
- Secret.h: This file should contain the MQTT username and password, which should be kept secure.
Hardware Setup #
Insert the registered SIM card to the module and upload the code.
Pin Connections
- MODEM_TX (TX Pin)
- MODEM_RX (RX Pin)
- GSM_RESET (Reset Pin)
- Digital Input Pins (D0, D1, D2, D3)
Understanding the Test program #
This code sets up an ESP32 microcontroller to connect to a GPRS network using a GSM module ( Quectel EC25), establish an MQTT connection, and handle sending and receiving MQTT messages. It uses the Arduino framework and various libraries to achieve this.
Download the example program.
1. Libraries and Includes
#include <Arduino.h> //Standard Arduino library.
#include <Wire.h> //Library for I2C communication.
#include <WiFi.h> // Include the WiFi library for MAC address
#include <ArduinoJson.h> //For handling JSON data.
#include "Secret.h" // Include the file to get the username and password of MQTT server
2. Definitions and Declarations
String gsm_send_serial(String command, int delay);
const char apn[] = "dialogbb"; // APN for the GPRS connection
//Address and port for the MQTT broker.
String broker = "*************"; //YOUR_MQTT_BROKER_ADDRESS
String MQTTport = "1881";
3. Pin Definitions and Sleep Settings
//Baud rate for serial communication with the EC25 module.
#define UART_BAUD 115200
//Pin assignments for the ESP32.
#define MODEM_TX 32
#define MODEM_RX 33
#define GSM_RESET 21
#define D0 34
#define D1 35
#define D2 14
#define D3 13
//Variables for storing the MAC address.
byte mac[6];
String str_macAddress;
//Used to control the timing of message publishing.
unsigned long prevMillis = 0;
const unsigned long interval = 60000; // Interval for sending messages
//Unique identifier for the device, used to filter incoming MQTT messages.
const char* deviceSerial = "3CE90E6C8F88"; // Replace with your device serial
4. Callback Function
This function checks if an incoming MQTT message is intended for the device by comparing MAC IDs. If they match, it processes the JSON payload and controls the relay (R0) based on the ‘state’ value.
void mqttCallback(char* topic, byte* payload, unsigned int len) {
SerialMon.print("Message arrived [");
SerialMon.print(topic);
SerialMon.print("]: ");
SerialMon.write(payload, len);
SerialMon.println();
// Extract serial number from the topic
String topicStr = String(topic);
int firstSlash = topicStr.indexOf('/');
int lastSlash = topicStr.lastIndexOf('/');
String MAC_ID = topicStr.substring(firstSlash + 1, lastSlash);
SerialMon.print("MAC ID: ");
SerialMon.println(MAC_ID);
if (MAC_ID == deviceSerial) {
// Decode the received message
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, payload, len);
if (error) {
SerialMon.print("deserializeJson() failed: ");
SerialMon.println(error.c_str());
return;
}
// Extract the payload
bool state = doc["state"];
SerialMon.print("STATE: ");
SerialMon.println(state);
// Handle state changes here
if (state == 0) {
digitalWrite(R0, LOW);
} else if (state == 1) {
digitalWrite(R0, HIGH);
}
} else {
SerialMon.println("Received message for a different serial number");
}
}
5. Setup Function
void setup() {
// Set console baud rate
Serial.begin(115200);
delay(10);
//initializes communication with the EC25 module.
SerialAT.begin(UART_BAUD, SERIAL_8N1, MODEM_RX, MODEM_TX);
delay(2000);
pinMode(GSM_RESET, OUTPUT);
digitalWrite(GSM_RESET, HIGH); // RS-485
delay(2000);
pinMode(D0, INPUT);
pinMode(D1, INPUT);
pinMode(D2, INPUT);
pinMode(D3, INPUT);
//establish network and MQTT connections.
Init();
connectToGPRS();
connectToMQTT();
}
6. Main Loop
This loop ensures that the device regularly publishes its input states to the MQTT broker and stays connected to the network, handling any disconnections or errors as they occur.
void loop() {
//checks if the interval has passed since the last message was published.
if (millis() - prevMillis >= interval) {
prevMillis = millis();
// Read input values (assuming these are commented out for now)
bool IN1 = digitalRead(D0);
bool IN2 = digitalRead(D1);
bool IN3 = digitalRead(D2);
bool IN4 = digitalRead(D3);
// Create JSON object
StaticJsonDocument<200> doc;
doc["D0"] = IN1 ? 1 : 0;
doc["D1"] = IN2 ? 1 : 0;
doc["D2"] = IN3 ? 1 : 0;
doc["D3"] = IN4 ? 1 : 0;
String jsonString;
serializeJson(doc, jsonString);
WiFi.macAddress(mac);
str_macAddress = (String(mac[0] >> 4, HEX) + String(mac[0] & 0x0F, HEX)) +
(String(mac[1] >> 4, HEX) + String(mac[1] & 0x0F, HEX)) +
(String(mac[2] >> 4, HEX) + String(mac[2] & 0x0F, HEX)) +
(String(mac[3] >> 4, HEX) + String(mac[3] & 0x0F, HEX)) +
(String(mac[4] >> 4, HEX) + String(mac[4] & 0x0F, HEX)) +
(String(mac[5] >> 4, HEX) + String(mac[5] & 0x0F, HEX));
str_macAddress.toUpperCase();
String Digital_input = "NORVI/INPUTS/" + str_macAddress; //MQTT Topic
SerialMon.print("Published: ");
SerialMon.println(jsonString);
// Publish MQTT message and check the result
String publishResponse = publishMQTTMessage(Digital_input,jsonString, 1, 0);
// QoS 1, retain 0
// Check if the publish command was successful
if (publishResponse.indexOf("ERROR") != -1) {
SerialMon.println("MQTT publish failed. Reconnecting...");
connectToMQTT();
if (!isGPRSConnected()) {
SerialMon.println("GPRS connection lost. Reconnecting...");
connectToGPRS();
connectToMQTT();
}
if (!isNetworkConnected()) {
SerialMon.println("Network connection lost. Reconnecting...");
Init();
connectToGPRS();
connectToMQTT();
}
}
}
// Handle incoming MQTT messages
handleIncomingMessages();
}
7. Publishing MQTT Messages
This function facilitates publishing MQTT messages via the EC25 module by constructing and sending the appropriate AT commands and payload.
String publishMQTTMessage(String topic, String message, int qos, int retain) {
// Convert topic and message to be used with AT+QMTPUBEX
int topicLength = topic.length();
int messageLength = message.length();
// Publish command
String command = "AT+QMTPUBEX=0,1," + String(qos) + "," + String(retain) + ",\"" + topic + "\"," + String(messageLength);
String response = gsm_send_serial(command, 1000);
// Send the actual payload
response += gsm_send_serial(message + "\x1A", 1000);
// Append end-of-message character if needed
// Print response for debugging
SerialMon.print("Publish Response: ");
SerialMon.println(response);
return response; // Return response for further handling
}
8. Handling Incoming Messages
This function handles incoming MQTT messages by requesting them from the EC25 module, parsing the response, and then processing the message with a callback function.
void handleIncomingMessages() {
// Request messages from the EC25 module
String response = gsm_send_serial("AT+QMTRECV=0,1", 1000);
// Print the raw response for debugging
SerialMon.print("Raw MQTT Response: ");
SerialMon.println(response);
// Check if the response contains "+QMTRECV:"
int startPos = response.indexOf("+QMTRECV:");
if (startPos != -1) {
// Extract the part of the response containing the message
String messagePart = response.substring(startPos);
// Print the extracted message part for debugging
SerialMon.print("Extracted Message Part: ");
SerialMon.println(messagePart);
// Remove any extraneous text before "+QMTRECV:"
messagePart.trim();
// Check if the response is in the expected format
if (messagePart.startsWith("+QMTRECV:")) {
// Extract the part after "+QMTRECV:" (skip the "+QMTRECV:" prefix)
messagePart = messagePart.substring(messagePart.indexOf(':') + 1);
// Extract client_idx and msg_id
int firstComma = messagePart.indexOf(',');
int secondComma = messagePart.indexOf(',', firstComma + 1);
String client_idx = messagePart.substring(0, firstComma);
String msg_id = messagePart.substring(firstComma + 1, secondComma);
// Extract topic
int firstQuote = messagePart.indexOf('"', secondComma + 1);
int secondQuote = messagePart.indexOf('"', firstQuote + 1);
String topic = messagePart.substring(firstQuote + 1, secondQuote);
// Extract payload length
int thirdComma = messagePart.indexOf(',', secondQuote + 1);
int fourthComma = messagePart.indexOf(',', thirdComma + 1);
String payloadLengthStr = messagePart.substring(thirdComma + 1, fourthComma);
int payloadLength = payloadLengthStr.toInt();
// Extract payload
int thirdQuote = messagePart.indexOf('"', fourthComma + 1);
int fourthQuote = messagePart.indexOf('}', thirdQuote + 1);
int fifthQuote = messagePart.indexOf('"', fourthQuote + 1);
String payload = messagePart.substring(thirdQuote + 1, fifthQuote );
// Debug print
SerialMon.print("Received Topic: ");
SerialMon.println(topic);
SerialMon.print("Received Payload: ");
SerialMon.println(payload);
SerialMon.print("Payload Length: ");
SerialMon.println(payloadLength);
// Convert topic and payload to mutable char arrays
char topicArr[topic.length() + 1];
byte payloadArr[payload.length() + 1];
topic.toCharArray(topicArr, topic.length() + 1);
for (int i = 0; i < payload.length(); ++i) {
payloadArr[i] = (byte)payload[i];
}
payloadArr[payload.length()] = 'void handleIncomingMessages() {
// Request messages from the EC25 module
String response = gsm_send_serial("AT+QMTRECV=0,1", 1000);
// Print the raw response for debugging
SerialMon.print("Raw MQTT Response: ");
SerialMon.println(response);
// Check if the response contains "+QMTRECV:"
int startPos = response.indexOf("+QMTRECV:");
if (startPos != -1) {
// Extract the part of the response containing the message
String messagePart = response.substring(startPos);
// Print the extracted message part for debugging
SerialMon.print("Extracted Message Part: ");
SerialMon.println(messagePart);
// Remove any extraneous text before "+QMTRECV:"
messagePart.trim();
// Check if the response is in the expected format
if (messagePart.startsWith("+QMTRECV:")) {
// Extract the part after "+QMTRECV:" (skip the "+QMTRECV:" prefix)
messagePart = messagePart.substring(messagePart.indexOf(':') + 1);
// Extract client_idx and msg_id
int firstComma = messagePart.indexOf(',');
int secondComma = messagePart.indexOf(',', firstComma + 1);
String client_idx = messagePart.substring(0, firstComma);
String msg_id = messagePart.substring(firstComma + 1, secondComma);
// Extract topic
int firstQuote = messagePart.indexOf('"', secondComma + 1);
int secondQuote = messagePart.indexOf('"', firstQuote + 1);
String topic = messagePart.substring(firstQuote + 1, secondQuote);
// Extract payload length
int thirdComma = messagePart.indexOf(',', secondQuote + 1);
int fourthComma = messagePart.indexOf(',', thirdComma + 1);
String payloadLengthStr = messagePart.substring(thirdComma + 1, fourthComma);
int payloadLength = payloadLengthStr.toInt();
// Extract payload
int thirdQuote = messagePart.indexOf('"', fourthComma + 1);
int fourthQuote = messagePart.indexOf('}', thirdQuote + 1);
int fifthQuote = messagePart.indexOf('"', fourthQuote + 1);
String payload = messagePart.substring(thirdQuote + 1, fifthQuote );
// Debug print
SerialMon.print("Received Topic: ");
SerialMon.println(topic);
SerialMon.print("Received Payload: ");
SerialMon.println(payload);
SerialMon.print("Payload Length: ");
SerialMon.println(payloadLength);
// Convert topic and payload to mutable char arrays
char topicArr[topic.length() + 1];
byte payloadArr[payload.length() + 1];
topic.toCharArray(topicArr, topic.length() + 1);
for (int i = 0; i < payload.length(); ++i) {
payloadArr[i] = (byte)payload[i];
}
payloadArr[payload.length()] = '\0'; // Null-terminate byte array
// Call the MQTT callback function with the extracted values
mqttCallback(topicArr, payloadArr, payload.length());
} else {
SerialMon.println("Unexpected response format.");
}
} else {
SerialMon.println("No new MQTT messages or unexpected response format.");
}
}
'; // Null-terminate byte array
// Call the MQTT callback function with the extracted values
mqttCallback(topicArr, payloadArr, payload.length());
} else {
SerialMon.println("Unexpected response format.");
}
} else {
SerialMon.println("No new MQTT messages or unexpected response format.");
}
}
9. Network and MQTT Initialization
These functions manage the connection setup for GPRS and MQTT, including network registration, GPRS attachment, MQTT configuration, and topic subscription.
// Initialize network connection using AT commands
void Init(void) {
// Connecting with the network and GPRS
delay(5000);
gsm_send_serial("AT+CFUN=1", 10000);
gsm_send_serial("AT+CPIN?", 10000);
gsm_send_serial("AT+CSQ", 1000);
gsm_send_serial("AT+CREG?", 1000);
gsm_send_serial("AT+COPS?", 1000);
gsm_send_serial("AT+CGATT?", 1000);
gsm_send_serial("AT+CPSI?", 500);
gsm_send_serial("AT+CGDCONT=1,\"IP\",\"dialogbb\"", 1000);
gsm_send_serial("AT+CGACT=1,1", 1000);
gsm_send_serial("AT+CGATT?", 1000);
gsm_send_serial("AT+CGPADDR=1", 500);
}
// Connect to GPRS using AT commands
void connectToGPRS(void) {
gsm_send_serial("AT+CGATT=1", 1000);
gsm_send_serial("AT+CGDCONT=1,\"IP\",\"dialogbb\"", 1000);
gsm_send_serial("AT+CGACT=1,1", 1000);
gsm_send_serial("AT+CGPADDR=1", 500);
}
// Configure MQTT settings and connect to the broker using AT commands
// Subscribe to the necessary topics
void connectToMQTT(void) {
// Initialize MQTT configurations
gsm_send_serial("AT+QMTCFG=\"recv/mode\",0,0,1", 1000);
gsm_send_serial("AT+QMTOPEN=0,\"mqtt.sensoper.net\",1881", 1000);
delay(2000);
String command = "AT+QMTCONN=0,\"EC25 client\",\"" + username + "\",\"" + password + "\"";
gsm_send_serial(command, 1000);
delay(2000); // Wait for the connection to establish
// Subscribe to the downlink topic
String downlinkTopic = "NORVI/+/OUTPUT";
String subscribeCommand = "AT+QMTSUB=0,1,\"" + downlinkTopic + "\",0";
// QoS 1
gsm_send_serial(subscribeCommand, 1000);
delay(2000); // Allow time for subscription confirmation
// Check for subscription confirmation
String response = gsm_send_serial("AT+QMTSUB?", 1000);
// Check subscription status
SerialMon.print("Subscription Response: ");
SerialMon.println(response);
// Debug: Print MQTT connection status
String connStatus = gsm_send_serial("AT+QMTCONN?", 1000);
SerialMon.print("MQTT Connection Status: ");
SerialMon.println(connStatus);
}
10. Utility Functions
These functions handle network and GPRS connectivity checks and provide a utility function to send AT commands to the GSM module and retrieve responses.
// Check if the network is connected by sending AT command
bool isNetworkConnected() {
String response = gsm_send_serial("AT+CREG?", 3000);
return (response.indexOf("+CREG: 0,1") != -1 || response.indexOf("+CREG: 0,5") != -1);
}
// Check if the GPRS is connected by sending AT command
bool isGPRSConnected() {
String response = gsm_send_serial("AT+CGATT?", 3000);
return (response.indexOf("+CGATT: 1") != -1);
}
// Send AT command and return the response
String gsm_send_serial(String command, int timeout) {
String buff_resp = "";
Serial.println("Send ->: " + command);
SerialAT.println(command);
unsigned long startMillis = millis();
while (millis() - startMillis < timeout) {
while (SerialAT.available()) {
char c = SerialAT.read();
buff_resp += c;
}
delay(10); // Small delay to allow for incoming data to accumulate
}
Serial.println("Response ->: " + buff_resp);
return buff_resp;
}
Software Configurations #
Essential AT Commands #
After uploading the code to the NORVI device, it’s important to verify that the SIM card is correctly registered and the device is able to communicate with the network.
This can be done by checking the serial monitor for specific AT command responses.
Signal Quality Check (AT+CSQ),
- Good Signal: An RSSI value of 18 indicates strong signal strength.
- Weak Signal: A lower value, like 5, suggests weak signal strength, which could cause connection issues.
Network Registration Check (AT+CREG?),
- Registered: The 1 indicates the device is registered on its home network.
- Not Registered: A response of 0,0 means the device isn’t registered and isn’t searching for a network, possib