Hello,
I have a program running on a Nano IoT 33 which runs through a STRUCT array of slave device addresses, registers and lengths and then records the result from the poll in the array. I then send the array by UDP on the LAN and also some posted to my Thinkspeak account. Main reason for wanting UDP on the LAN is so displays of the MODBUS data are live and fast responding.
What's happening just now i see are two issues related to timing;
-
One of the slave devices (a solar PV inverter) does not respond when its dark, so the MODBUS library 1000ms timeout slows everything down. As far as i can see there's not a way to change this timeout.
-
Even when the solar inverter is responding, the MODBUS loop takes quite a period of time. The baud rate is 9600 I've attached the comms protocol for one inverter here, it suggests a 300ms time required between requests, it looks like its taking about 90-100ms.
RS485_MODBUS Communication Protocol_Solis Inverters.pdf (592.3 KB)
I have a couple of thoughts but need some forum assistance;
-
Can the library be edited and any suggestions for how to go about this?
-
a. I could do a smaller UDP transmission after each MODBUS poll, that puts more onus on the receiver to then file the received UDP data for use by those end points
-
b. The devices permit sets of 50 registers to be read as a range, i would think this has less overhead as its one request as opposed to 50, and the reply will have less overhead too. Given some registers are single and others are double, i am not sure how to receive these 50 and then parse into a 16bit or 32bit values
-
c. Some registers done change very often, so i had looked to poll a smaller set of values quickly for the real time display, then perhaps every minute, poll the full table. This is partially in place in the code below before i realised the issues would then be present and it may be better to make things more efficient first.
I would appreciate any suggestions and ideas to help.
The full code as follows, at this time all libraries are current versions with no changes;
/*
Solis RAI inverter/charger unit and Solis MINI Solar PV inverter RS485 MODBUS to network interface
S. Graham May/June 2023
based around the excellent work of;
R.A.Lincoln July 2022, SolisComms https://github.com/RichardL64
Hardware: Arduino Nano 33 IOT
SolisComms_SGr_
V1
V2
V3 These versions added in polling a second address for the solar PV and send by HTTP/UDP etc - too many issues with mods
V1_1_Cutback Start a fresh - remove the things i dont need, eg the webserver
V1_2_Cutback Bit more work done
V4 from V1.2 Got this working, polling a long list of registers on both addresses, posting to thingspeak and also doing a UDP broadcast
Issues with timing:
When theres no PV, the MODBUS library 1000ms timeout is too long - not fussed; need perhaps 20ms perhaps
UDP and Thingspeak held up by MODBUS polling, need to do the UDP quicker, thingspeak probably OK.
V5 Lets try and fix things!
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
//******************* SYSTEM STUFF ******************* // //******************* SYSTEM STUFF ******************* //
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* LIBRARIES ******************* //
*/
#include <SPI.h>
#include <WiFiNINA.h> // (note _generic version locks up on closed connections)
#include <ArduinoModbus.h>
#include <ModbusClient.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <NTPClient.h>
#include "ThingSpeak.h" // always include thingspeak header file after other header files and custom macros
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* WiFi HEADER ******************* //
#define SECRET_SSID "removed"
#define SECRET_PASS "removed"
// Various objects which will use WiFi/data comms
WiFiClient client; // used for thingspeak
WiFiUDP ntpUDP; // used for NTP receie server
WiFiUDP lanUDP; // used for LAN transmission
int statusTx = 0; //transmission status
IPAddress ip(192, 168, 1, 61);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);
IPAddress primaryDNS(8, 8, 8, 8); //optional
IPAddress secondaryDNS(8, 8, 4, 4); //optional
IPAddress destIp(192, 168, 1, 255);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* TIMERS ******************* //
unsigned long currentMicros = 0;
unsigned long currentMillis = 0;
//2Hz / 500ms loop
unsigned long prevMillis2Hz = 0;
unsigned long millisPer2Hz = 500; // 500ms
//1Hz / 1000ms loop
unsigned long prevMillis1Hz = 0;
unsigned long millisPer1Hz = 1000; // 1000ms
// program timer
int progTime = 0;
int timeEnd = 0;
int loopDelay = 5; // delay in main loop to allow background processes
int counter = 0;
//display timer
unsigned long delaytime = 500;
unsigned long delayprint = 100;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* NTP TIME HEADER ******************* //
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", (60 * 60), 60000); // update ever minute, add 1 hour for BST
int liveDays = 0;
int liveHours = 0;
int liveMinutes = 0;
int liveSeconds = 0;
int liveMinutesRunning = 0;
unsigned long unix = 0;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* THINGSPEAK HEADER ******************* //
unsigned long solarBattery = removed;
const char * solarBatteryStatusWriteAPIKey = "removed";
String solarBatteryStatus = "";
int updatePeriod = 2; // number of 1Hz loops between transmissions
int periods = 0; // counter for the updatePeriod for
unsigned long pktTxTspk = 0; // Transmit Packet counter
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* UDP ******************* //
char packetBuffer[255];
unsigned int localPort = 1912;
unsigned int remotePort = 1912;
unsigned long pktTxUDP = 0; // Transmit Packet counter
unsigned long pktRxUDP = 0; // Receive Packet counter
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* MODBUS ******************* //
#define MODBUS_DELAY 5
struct database
{
int addr;
int command;
int registerNum;
int length;
long returnedValue;
};
const int storagePVSize = 5;
database storagePV[storagePVSize] = {
2, 4, 3004, 2, 0, //PV inverter active AC power
1, 4, 33079, 2, 0, //storage inverter active AC power (unsigned)
1, 4, 33135, 1, 0, //Battery current direction
1, 4, 33139, 1, 0, //Battery capacity
1, 4, 33130, 2, 0, //grid meter active power (signed, negative as import)
};
const int fullSystemSize = 50;
database fullSystem[fullSystemSize] = {
2, 4, 3035, 1, 0, //AC Voltage
2, 4, 3038, 1, 0, //AC Current
2, 4, 3042, 1, 0, //AC Frequency
2, 4, 3021, 1, 0, //DC Voltage
2, 4, 3022, 1, 0, //DC Current
2, 4, 3014, 1, 0, //Today energy
2, 4, 3041, 1, 0, //Inverter Temperature
1, 4, 33073, 1, 0, //AB line voltage /
1, 4, 33076, 1, 0, //Phase A current
1, 4, 33081, 2, 0, //Reactive power
1, 4, 33083, 2, 0, //Apparent power
1, 4, 33094, 1, 0, //Grid frequency
1, 4, 33137, 1, 0, //Bypass AC voltage
1, 4, 33138, 1, 0, //Bypass AC current
1, 4, 33148, 1, 0, //Bypass load power
1, 4, 33057, 2, 0, //Total DC output power
1, 4, 33133, 1, 0, //Battery voltage
1, 4, 33134, 1, 0, //Battery current
1, 4, 33136, 1, 0, //LLCbus voltage
1, 4, 33149, 2, 0, //Battery power
1, 4, 33161, 2, 0, //Total battery charge
1, 4, 33163, 1, 0, //Battery charge today
1, 4, 33164, 1, 0, //Battery charge yesterday
1, 4, 33165, 2, 0, //Total battery discharge
1, 4, 33167, 1, 0, //Battery discharge capacity
1, 4, 33168, 1, 0, //Battery discharge power yesterday
1, 4, 33169, 2, 0, //Total power imported from Grid
1, 4, 33171, 1, 0, //Grid power imported today
1, 4, 33172, 1, 0, //Grid power imported yesterday
1, 4, 33173, 2, 0, //Total power exported to Grid
1, 4, 33175, 1, 0, //Power imported from Grid today
1, 4, 33176, 1, 0, //Power imported from Grid yesterday
1, 4, 33141, 1, 0, //BMS Battery Voltage
1, 4, 33142, 1, 0, //BMS Battery Current
1, 4, 33126, 2, 0, //Electricity meter total active power generation
1, 4, 33128, 1, 0, //Meter voltage
1, 4, 33129, 1, 0, //Meter current
1, 4, 33281, 1, 0, //Meter power factor
1, 4, 33282, 1, 0, //Meter Grid frequency
1, 4, 33283, 1, 0, //Meter total active energy imported from Grid
1, 4, 33285, 2, 0, //Meter total active energy exported to Grid
1, 4, 33022, 1, 0, //System time year
1, 4, 33023, 1, 0, //System time month
1, 4, 33024, 1, 0, //System time day
1, 4, 33025, 1, 0, //System time
1, 4, 33026, 1, 0, //System time
1, 4, 33027, 1, 0, //System time second
1, 4, 33071, 1, 0, //DC bus voltage
1, 4, 33093, 1, 0, //Inverter
1, 4, 33095, 1, 0 //Current state of the inverter
};
//================================================================================================================================
/////////////////////////////////////////////////////////
//*********************** SETUP ********************** //
void setup() {
Serial.begin(9600); // initialize serial communication
pinMode(LED_BUILTIN, OUTPUT); // LED control
digitalWrite(LED_BUILTIN, HIGH); // LED lit during setup - should go out if sucessful
// if (!WiFi.config(ip, gateway, subnet, primaryDNS, secondaryDNS)) {
// Serial.println("STA Failed to configure");
// }
setupWiFi(); // Bring WiFi and mDNS up
digitalWrite(LED_BUILTIN, HIGH); // LED lit during setup - should go out if sucessful
Serial.println(F("Modbus begin")); // Bring Modbus up
if (!ModbusRTUClient.begin(9600)) {
Serial.println(F("Modbus RTU Client start failed"));
while (true); // lockup
}
lanUDP.begin(localPort); // Initialise local LAN UDP
ThingSpeak.begin(client); // Initialise ThingSpeak
digitalWrite(LED_BUILTIN, LOW);
}
//================================================================================================================================
// Wifi per R.A.Lincoln | July 2022 >>
void setupWiFi() {
digitalWrite(LED_BUILTIN, HIGH); // LED lit during setup - should go out if sucessful
Serial.println(F("WiFi begin")); // Bring WiFi up
while (WiFi.status() != WL_CONNECTED) {
WiFi.begin(SECRET_SSID, SECRET_PASS);
delay(4000);
}
printWifiStatus();
digitalWrite(LED_BUILTIN, LOW);
}
void printWifiStatus() {
// print the SSID of the network you're attached to:
Serial.print(F("SSID: "));
Serial.println(WiFi.SSID());
// print your board's IP address:
IPAddress ip = WiFi.localIP();
Serial.print(F("IP Address: "));
Serial.println(ip);
// print the received signal strength:
long rssi = WiFi.RSSI();
Serial.print(F("Signal strength (RSSI):"));
Serial.print(rssi);
Serial.println(F(" dBm"));
}
//================================================================================================================================
void loop() {
/////////////////////////////////////?///////////////////////
//******************* MAIN LOOP TIMER ******************* //
delay(loopDelay);
currentMillis = millis(); // use the same Millis for all timing each loop, i.e. dont end up with many Millisses, apart from...
currentMicros = micros(); // where we use micros to do the program time
progTime = currentMicros - timeEnd; // will use the current micros at the end of the loop
//******************* REAL TIME LOOP ******************* //
if (WiFi.status() != WL_CONNECTED) setupWiFi(); // re-connect if disconnected
static unsigned long lastCollect;
for ( int j = 0; j < storagePVSize; ++j ) // just use the numbers just now, will need to be more dynamic
{
delay(MODBUS_DELAY);
Serial.print("StoragePVPoll ");
Serial.print(j);
digitalWrite(LED_BUILTIN, HIGH);
int pollAddr = storagePV[j].addr;
int pollCommand = storagePV[j].command;
int pollRegisterNum = storagePV[j].registerNum;
int pollLength = storagePV[j].length;
long pollReturnedValue = 0;
Serial.print(", ");
Serial.print(pollAddr);
Serial.print(", ");
Serial.print(pollCommand);
Serial.print(", ");
Serial.print(pollRegisterNum);
Serial.print(", ");
Serial.print(pollLength);
Serial.print(", ");
Serial.print(pollReturnedValue);
Serial.print(", ");
if (!ModbusRTUClient.requestFrom(pollAddr, INPUT_REGISTERS, pollRegisterNum, pollLength))
{
Serial.println(F(" = Data error"));
}
else
{
if (pollLength == 2) pollReturnedValue = ModbusRTUClient.read() << 16; // 32 bit High 16
pollReturnedValue |= ModbusRTUClient.read(); // Low 16 bits
storagePV[j].returnedValue = pollReturnedValue;
Serial.print(pollReturnedValue);
Serial.println(", ");
}
digitalWrite(LED_BUILTIN, LOW);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* 2Hz TIMER LOOP ******************* //
if ((currentMillis - prevMillis2Hz) >= millisPer2Hz)
{
prevMillis2Hz = currentMillis;
lanUDP.beginPacket(destIp, remotePort);
lanUDP.write((byte*)storagePV, sizeof(storagePV));
lanUDP.endPacket();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************* 1Hz TIMER LOOP ******************* //
if ((currentMillis - prevMillis1Hz) >= millisPer1Hz)
{
prevMillis1Hz = currentMillis;
periods = periods + 1;
Serial.print("1Hz| ");
Serial.println(periods);
for ( int j = 0; j < fullSystemSize; ++j ) // just use the numbers just now, will need to be more dynamic
{
delay(MODBUS_DELAY);
Serial.print("SystemPoll ");
Serial.print(j);
digitalWrite(LED_BUILTIN, HIGH);
int pollAddr = fullSystem[j].addr;
int pollCommand = fullSystem[j].command;
int pollRegisterNum = fullSystem[j].registerNum;
int pollLength = fullSystem[j].length;
long pollReturnedValue = 0;
Serial.print(", ");
Serial.print(pollAddr);
Serial.print(", ");
Serial.print(pollCommand);
Serial.print(", ");
Serial.print(pollRegisterNum);
Serial.print(", ");
Serial.print(pollLength);
Serial.print(", ");
Serial.print(pollReturnedValue);
Serial.print(", ");
if (!ModbusRTUClient.requestFrom(pollAddr, INPUT_REGISTERS, pollRegisterNum, pollLength))
{
Serial.println(F(" = Data error"));
}
else
{
if (pollLength == 2) pollReturnedValue = ModbusRTUClient.read() << 16; // 32 bit High 16
pollReturnedValue |= ModbusRTUClient.read(); // Low 16 bits
fullSystem[j].returnedValue = pollReturnedValue;
Serial.print(pollReturnedValue);
Serial.println(", ");
}
digitalWrite(LED_BUILTIN, LOW);
}
/////////////////////////////////////////////////////////////////////
//******************* THINGSPEAK TRANSMISSION ******************* //
if (periods >= updatePeriod)
{
periods = 0; // reset the periods counter
pktTxTspk = pktTxTspk + 1; // transmission loops counter
// SolarStorage values transmission
ThingSpeak.setField(1, storagePV[0].returnedValue); //time between gas pulses
ThingSpeak.setField(2, storagePV[1].returnedValue); //
ThingSpeak.setField(3, storagePV[2].returnedValue); //
ThingSpeak.setField(4, storagePV[3].returnedValue); //
ThingSpeak.setField(5, storagePV[4].returnedValue); //
ThingSpeak.setStatus(solarBatteryStatus);
// write to the ThingSpeak channel
int solarStoragex = ThingSpeak.writeFields(solarBattery, solarBatteryStatusWriteAPIKey);
if (solarStoragex == 200) {
Serial.println("Solar Storage Channel update successful.");
}
else {
Serial.println("Solar Storage Problem updating channel; " + String(solarStoragex));
}
}
} // ====== END OF 1Hz LOOP ++++++
} // ====== END OF MAIN LOOP ++++++
//================================================================================================================================