Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
philippedc
Published © GPL3+

Arduino UNO R3 / ESP8266 RS485 MODBUS Anemometer

The objective is to find an easy way to implement RS485 on an Arduino Uno R3, then to adapt it to an ESP8266.

IntermediateFull instructions provided48,054
Arduino UNO R3 / ESP8266 RS485 MODBUS Anemometer

Things used in this project

Hardware components

Arduino UNO
Arduino UNO
×1
Wemos D1 Mini
Espressif Wemos D1 Mini
×1
DS1307 and SD card shield
×1
MAX485
×1
I2C 16x2 Arduino LCD Display Module
DFRobot I2C 16x2 Arduino LCD Display Module
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Mastech MS8217 Autorange Digital Multimeter
Digilent Mastech MS8217 Autorange Digital Multimeter
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

Arduino-ESP8266-RS485-MODBUS-Anemometer

Everything you will need to understand this project

Schematics

arduino anemometer

Arduino Uno Anemometer

ESP8266 wemos D1 mini diagram

ESP8266 wemos D1 mini diagram

Code

Arduino-ESP8266-RS485-MODBUS-Anemometer

C/C++
this code is for Arduino Uno R3
/*
Anemometer with a RS485 wind sensor

from an idea of https://arduino.stackexchange.com/questions/62327/cannot-read-modbus-data-repetitively
https://www.cupidcontrols.com/2015/10/software-serial-modbus-master-over-rs485-transceiver/

_________________________________________________________________
|                                                               |
|       author : Philippe de Craene <dcphilippe@yahoo.fr        |
|       Any feedback is welcome                                 |
                                                                |
_________________________________________________________________

Materials :
 1* Arduino Uno R3 - tested with IDE version 1.8.7 and 1.8.9
 1* wind sensor - RS485 MODBUS protocol of communication
 1* MAX485 DIP8

Versions chronology:
version 0.1  - 7 sept  2019   - first test 
version 0.2  - 7 nov 2023     - change RX TX pin connection
version 0.3  - 7 nov 2023     - add high bit to wind speed measure

*/

#define RX         2     // Serial Receive pin
#define TX         3     // Serial Transmit pin
#define RTS        4     // RS485 Direction control
#define RS485Transmit    HIGH
#define RS485Receive     LOW

#include <SoftwareSerial.h> 
SoftwareSerial RS485Serial(RX, TX);

const byte request[] = {0x01, 0x03, 0x00, 0x16, 0x00, 0x01, 0x65, 0xCE}; // inquiry frame

void setup() {

  pinMode(RTS, OUTPUT);  
  
  // Start the built-in serial port, for Serial Monitor
  Serial.begin(250000);
  Serial.println("Anemometer"); 

  // Start the Modbus serial Port, for anemometer
  RS485Serial.begin(9600);   
  delay(1000);
}

void loop() {
  
  byte response[7] = { 0, 0, 0, 0, 0, 0, 0};
  while( !((response[0] == 0x01)&&(response[1] == 0x03)&&(response[2] == 0x02))) {   // if received message has an error
    // MODBUS Tramsmit by sending a request to the anemometer
    digitalWrite(RTS, RS485Transmit);     // init Transmit
    RS485Serial.write(request, sizeof(request));
    RS485Serial.flush();
    // MODBUS Reception of the anemometer's answer
    digitalWrite(RTS, RS485Receive);     // init Receive
    RS485Serial.readBytes(response, sizeof(response));
    delay(100);
  }
  for( byte i=0; i<(sizeof(response)); i++ ) {
    Serial.print(response[i], HEX);
    Serial.print(" ");
  }
  float windSpeed = (float)((response[3] << 8) + response[4])/10.0;
  Serial.print("\t==> ");
  Serial.print(windSpeed);
  Serial.print(" m/s");
  Serial.println();                  
}

wemos D1 mini code

C/C++
this code is for ESP8266
/*
Anemometer from an idea of https://arduino.stackexchange.com/questions/62327/cannot-read-modbus-data-repetitively
https://www.cupidcontrols.com/2015/10/software-serial-modbus-master-over-rs485-transceiver/

_________________________________________________________________
|                                                               |
|       author : Philippe de Craene <dcphilippe@yahoo.fr        |
|       Any feedback is welcome                                 |
                                                                |
_________________________________________________________________

Materials :
 1* Wemos D1 mini - tested with IDE version 1.8.7 and 1.8.9
 1* wind sensor - RS485 MODBUS protocol of communication
 1* MAX485 DIP8
 1* RTC 1307
 1* LCD1602 with I2C extension
 1* SD card


Versions chronology:
version 1   - 7 sept 2019   - first test on Arduino Uno
Version 3   - 9 sept 2019   - ESP8266 based with RTC and SD card


ESP8266 pinup :

D1 => SCL for LCD1602 and DS1307 (Arduino A5) 
D2 => SDA for LCD1602 and DS1307 (Arduino A4)

D3 => Rx = RO of MAX485 - pin 1
D4 => Tx = DI of MAX485 - pin 4
D8 => RTS = RE/DE of MAX485 - pins 2&3

D5 => SCK for SDcard  (Arduino 13)
D6 => MISO for SDcard (Arduino 12)
D7 => MOSI for SDcard (Arduino 11)
D0 => CS for SDcard (SDcard Arduino shield 10) CS should be in D8 but must be at 0 
      during boot, but stay stuck at Vcc....

*/

#include <ESP8266WiFi.h>       // https://github.com/esp8266/Arduino
#include <WiFiUdp.h>
#include <ESP8266WebServer.h>  // required pour WifiManager.h
#include <DNSServer.h>         // required pour WifiManager.h
#include <WiFiManager.h>       // https://github.com/tzapu/WiFiManager
#include <ArduinoOTA.h>        // https://github.com/marcudanf/arduinoOTA
#include <TimeLib.h>           // https://github.com/PaulStoffregen/Time
#include <DS1307RTC.h>         // https://github.com/PaulStoffregen/DS1307RTC
#include <SD.h>                // yet include : https://github.com/adafruit/SD
#include <SoftwareSerial.h>    // https://github.com/PaulStoffregen/SoftwareSerial
#include <LiquidCrystal_I2C.h> // https://github.com/lucasmaziero/LiquidCrystal_I2C

#define RX        D3    // Soft Serial RS485 Receive pin
#define TX        D4    // Soft Serial RS485 Transmit pin
#define RTS       D8    // RS485 Direction control
#define RS485Transmit    HIGH
#define RS485Receive     LOW
#define CS        D0    // CS for SDcard

SoftwareSerial RS485Serial(RX, TX);    // additional serial port for RS485
WiFiServer server(80);                 // web server on www default port 80
LiquidCrystal_I2C lcd(0x27, 16, 2);    // Set the LCD address to 0x27 for a 16 chars and 2 line display
File dataFile;                         // initialisation of the SD card

// NTP server declaration
int TZ = 2;                            // timezone
unsigned int localPort = 2390;         // local port to listen for UDP packets
/* Don't hardwire the IP address or we won't get the benefits of the pool.
    Lookup the IP address for the host name instead */
//IPAddress timeServer(129, 6, 15, 28);   // time.nist.gov NTP server
IPAddress timeServerIP;                 // time.nist.gov NTP server address
const char* ntpServerName = "time.nist.gov";
const int NTP_PACKET_SIZE = 48;         // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE];    // buffer to hold incoming and outgoing packets
WiFiUDP udp;                            // A UDP instance to let us send and receive packets over UDP

// Variables declaration
float Anemometer = 0, memo_Anemometer = 0;
char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
bool afficher = true;        // affichage sur LCD
unsigned int delai = 2000;   // delay between 2 measures in ms
unsigned int memo_actuel = 0;

//
// SETUP
//_____________________________________________________________________________________________

void setup() {

  pinMode(RTS, OUTPUT);  
  pinMode(CS, OUTPUT);
  
// Start the built-in serial port, for Serial Monitor
  Serial.begin(9600);
  Serial.println("Anemometer"); 

// Start the Modbus serial Port, for anemometer
  RS485Serial.begin(9600);   
  delay(100);

// initialize the LCD
  lcd.begin();                  // Init with pin default ESP8266 or ARDUINO
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Anemometer");
  lcd.setCursor(0, 1);

// see if the RTC is present and is set
  tmElements_t tm;
  if (RTC.read(tm)) {
    Serial.print("Ok, Time = ");
    Serial.print(tm.Hour); Serial.write(':');
    Serial.print(tm.Minute); Serial.write(':');
    Serial.print(tm.Second);
    Serial.print(", Date (D/M/Y) = ");
    Serial.print(tm.Day);
    Serial.write('/');
    Serial.print(tm.Month);
    Serial.write('/');
    Serial.print(tmYearToCalendar(tm.Year));
    Serial.println();
    setSyncProvider(RTC.get);     // to get the time from the RTC
    lcd.print("Time: OK    ");
  } 
  else {
    if (RTC.chipPresent()) Serial.println("The DS1307 is stopped.  Please set time");
    else Serial.println("DS1307 read error!  Please check the circuitry.");
    lcd.print("Time: FAIL  ");
  }
  Serial.println();
  delay(1000);
  lcd.setCursor(0, 1);

// see if the card is present and can be initialized:
  if (!SD.begin(CS)) Serial.println("Card failed, or not present"); 
  else { Serial.println("card initialized."); }
  // Open up the file we're going to log to!
  dataFile = SD.open("datalog.txt", FILE_WRITE);
  if (!dataFile) { 
    Serial.println("datalog.txt error !"); 
    lcd.print("SDcard: FAIL");
  }
  else {
    Serial.println(" datalog.txt ready ...");
    lcd.print("SDcard: OK  ");
  }
  Serial.println();
  delay(1000);
  lcd.setCursor(0, 0);
  lcd.print("WiFi is     ");
  lcd.setCursor(0, 1);
  lcd.print("starting ...");

// AP will start if no wifi identifiers in memory or wrong identification
// AP can be accessed from ssid "AutoConnectAP" then IP address 192.168.4.1 within 150 seconds
// in cas of unsuccess after 150 seconds the wifi will not be defined
// for local intialization. Once its business is done, there is no need to keep it around
  WiFiManager monwifi;
  monwifi.setConfigPortalTimeout(180);    // 150 seconds timeout
  byte i = 0;                         // counter of request to wifi connexion
  byte imax = 10;                     // max number of request to wifi connexion
// fetches ssid and pass from eeprom and tries to connect. If it does not connect it starts 
// an access point with the specified name and goes into a blocking loop awaiting configuration
  if(!monwifi.autoConnect("AutoConnectAP")) Serial.println("non paramtr");
  else {
// Connect to Wi-Fi network with SSID and password
    Serial.print("connexion au Wifi en cours ");               
    while( (WiFi.status() != WL_CONNECTED) && i < imax ) { 
      i++;
      delay(500); 
      Serial.print(".");
    }
  }     // end of else monwifi.autoConnect

// if wifi is connected
  if( i < imax ) {
// show IP address
    Serial.println();
    Serial.println("Wifi connect.");
    Serial.print("Address IP : ");
    Serial.println(WiFi.localIP());
    lcd.setCursor(0, 1);
    lcd.print("started !   ");
      
// 2 of the 3 lines of code for OTA
    ArduinoOTA.setHostname("Anemometer");  // device name 
    ArduinoOTA.begin();                    // OTA initialisation

// udp service startup    
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());

  }  // end of test i
  else {
    Serial.println();
    Serial.println("pas de rseau wifi");
    Serial.println("Rcupration de l'heure en local");
    lcd.setCursor(0, 1);
    lcd.print("not started ");
    delay(1000);
  }
  server.begin();             // web server startup
 // getNTP();                 // NTP function to get the internet date and time
  lcd.setCursor(0, 0);
  lcd.print("Anemometer");
  lcd.setCursor(0, 1);
  
}  // end of setup

//
// LOOP
//_____________________________________________________________________________________________

void loop() {

// The 3rd code line for OTA
  ArduinoOTA.handle();

// to display data on a html page
  webserver();      

// Daily time update
  if( hour() == 1 && minute() == 0 && second() < 2 ) getNTP();

// The above of the loop is done every waitdelay seconds only
  unsigned int actuel = millis();
  if( actuel - memo_actuel < delai ) return;
  memo_actuel = actuel; 

// RS485 MODBUS Request and Receive with the anemometer
  byte Anemometer_buf[8];
  Anemometer_buf[1] = 0;
  while( Anemometer_buf[1] != 0x03 ) {    // if received message has an error
    // MODBUS Tramsmit by sending a request to the anemometer
    digitalWrite(RTS, RS485Transmit);     // init Transmit
    byte Anemometer_request[] = {0x01, 0x03, 0x00, 0x16, 0x00, 0x01, 0x65, 0xCE}; // inquiry frame
    RS485Serial.write(Anemometer_request, sizeof(Anemometer_request));
    RS485Serial.flush();
    // MODBUS Reception of the anemometer's answer
    digitalWrite(RTS, RS485Receive);      // init Receive
    RS485Serial.readBytes(Anemometer_buf, 8);
    // data treatment
    Serial.print("wind speed : ");
    for( byte i=0; i<7; i++ ) {
      Serial.print(Anemometer_buf[i], HEX);
      Serial.print(" ");
    }
    Serial.print(" ==> ");
    Serial.print(Anemometer_buf[4]);
    Serial.print(" /10 m/s");
    Serial.println(); 
    delay(500);
  }   // end of while 
  memo_Anemometer = Anemometer;
  Anemometer = Anemometer_buf[4]/10.0;
  lcd.setCursor(0, 1);
  lcd.print(Anemometer);
  lcd.print(" m/s      ");   

// Store on SDcard
  if( Anemometer != memo_Anemometer ) {  // if wind speed change
    String dataString = "";              // initialisation d'une chaine de caractres
    dataString += String(daysOfTheWeek[weekday()-1]);
    dataString += ";";
    dataString += String(day(), DEC);
    dataString += ";";
    dataString += String(month(), DEC);
    dataString += ";";
    dataString += String(year(), DEC);
    dataString += ";";
    dataString += String(hour(), DEC);
    dataString += ";";
    dataString += String(minute(), DEC);
    dataString += ";";
    dataString += String(second(), DEC);
    dataString += ";";
    dataString += String(Anemometer);
    dataString += ";";
    dataFile.println(dataString);     // record data on SD card
    dataFile.flush();                 // clean buffer
    Serial.println(dataString);       // show record on console
  }   // end test Anemomter
}     // end of loop

//
// webserver : display data on html page
//____________________________________________________________________________________________

void webserver() { 
  
  WiFiClient client = server.available();   // Listen for incoming clients
  if( client ) {                            // If a new client connects,
    Serial.println("Nouveau client.");      // print a message out in the serial port
    
    String entete = client.readStringUntil('\r'); // read the header until \r
    Serial.print("header received => ");
    Serial.println(entete); 
    
    String etat_afficher[] = {"non", "oui"};
    if( entete.indexOf("GET /?A=0") >= 0) afficher = false;
    if( entete.indexOf("GET /?A=1") >= 0) afficher = true;
    Serial.print("\n Etat de l'affichage du LCD : ");
    Serial.println(afficher);
    if( afficher == true ) lcd.backlight();
    else lcd.noBacklight();
    
    client.flush();                         //nettoie le tampon...
    // HTTP header
    client.println("HTTP/1.1 200 OK");
    client.println("Content-type:text/html");
    client.println("Connection: close");
    client.println();
    // Display the HTML web page with every 4 seconds refraish 
    client.println("<!DOCTYPE html><html lang=fr-FR>");
    client.println("<head><meta http-equiv='refresh' content='4'/>");
    client.println("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
    client.println("<link rel=\"icon\" href=\"data:,\">");
    // CSS to style the on/off buttons 
    client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
    client.println(".button { background-color: #8A0808; border: none; color: white; padding: 16px 40px;");
    client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
    client.println(".button2 {background-color: #32CD99;}");
    client.println(".button3 {background-color: #08298A;}</style></head>");
    // Web Page Heading
    client.println("<body><h1>An&eacute;mometre chez Fifi</h1>");
    String Minutes = "0";
    if( minute() < 10 ) Minutes += String(minute());
    else Minutes = String(minute());
    client.println("<p><h3>Il est " + String(hour()) + "h" + Minutes + " et " + String(second()) + " secondes;</h3></p>");
    client.println("<HR size=2 align=center width=\"80%\">");
    client.println("<p><h3>Vitesse du vent " + String(Anemometer) + " m/s</h3></p>");
    client.println("<HR size=2 align=center width=\"80%\">");
    client.println("<p><h2>Affichage : " + etat_afficher[afficher] +"</h2></p>");
    client.println("<FORM>");
    client.println("<INPUT type=\"radio\" name=\"A\" value=\"1\">Allumer");
    client.println("<INPUT type=\"radio\" name=\"A\" value=\"0\">Eteindre");
    client.println("<INPUT class=\"button button3\" type=\"submit\" value=\"Actualiser\"></FORM>");
    client.println("</BODY></center></html>");
    client.println();              // The HTTP response ends with another blank line
            
    Serial.println("Fin de transmission web - Client disconnected.");
    Serial.println("");
  }  // end of client
}   // end of webserver

//
// getNTP : to get date and time from internet
//____________________________________________________________________________________________

void getNTP() {

  byte i = 0;             // NTP request counter
  byte imax = 40;         // max number of request
  
  WiFi.hostByName(ntpServerName, timeServerIP);  // get a random server from the pool
  
  do {
     i++;
     Serial.print("sending NTP packet... ");
     Serial.println(i);
     memset(packetBuffer, 0, NTP_PACKET_SIZE);   // set all bytes in the buffer to 0
     // Initialize values needed to form NTP request
     packetBuffer[0] = 0b11100011;   // LI, Version, Mode
     packetBuffer[1] = 0;            // Stratum, or type of clock
     packetBuffer[2] = 6;            // Polling Interval
     packetBuffer[3] = 0xEC;         // Peer Clock Precision
     // 8 bytes of zero for Root Delay & Root Dispersion
     packetBuffer[12] = 49;
     packetBuffer[13] = 0x4E;
     packetBuffer[14] = 49;
     packetBuffer[15] = 52;
     // all NTP fields have been given values, now you can send a packet requesting a timestamp:
     udp.beginPacket(timeServerIP, 123);       // NTP requests are to port 123
     udp.write(packetBuffer, NTP_PACKET_SIZE);
     udp.endPacket();
     delay(1000);                              // wait to see if a reply is available
  } while(!udp.parsePacket() && i<imax); 

  if( i<imax ) {                     // We've received a packet, read the data from it
    udp.read(packetBuffer, NTP_PACKET_SIZE);     // read the packet into the buffer
 
    //the timestamp starts at byte 40 of the received packet and is four bytes,
    // or two words, long. First, esxtract the two words:
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
    // combine the four bytes (two words) into a long integer
    // this is NTP time (seconds since Jan 1 1900):
    unsigned long secsSince1900 = highWord << 16 | lowWord;
    Serial.print("Seconds since Jan 1 1900 = ");
    Serial.println(secsSince1900);

    // now convert NTP time into everyday time:
    Serial.print("Unix time = ");
    // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
    const unsigned long seventyYears = 2208988800UL;
    // subtract seventy years:
    unsigned long epoch = secsSince1900 - seventyYears;
    // print Unix time:
    Serial.println(epoch);
    
    // // to see if it is summer or winter time
    int mois = month();
    int jour = day();
    int joursemaine = weekday();
    if( mois > 3 || mois < 10  
      || (mois == 3  && (jour - joursemaine) > 22 )
      || (mois == 10 && (jour - joursemaine) < 23 ) ) TZ = 2;    
    else TZ = 1;                   // heure d'hiver
    RTC.set(epoch + TZ*3600);
    setTime(epoch + TZ*3600);       // date and time adjust 
  }
  else setSyncProvider(RTC.get);    // the function to get the time from the RTC
}  //   end of getNTP
  

Credits

philippedc

philippedc

8 projects • 74 followers

Comments