ESP8266 Wemos D1 mini Modbus RTU (RS485) Slave Device/ Arduino IDE (One Wire, Digital In, Digital Out)

Hello everyone,
as I couldn’t really find what I was looking for on the net, I would like to post my ESP8266 Wemos D1 mini project here as an example.
I use a Wemos D1 mini together with a TTL to RS485 converter and a RS485 converter to USB as Modbus slave, Home Assistant acts as Modbus master.

The code of the Wemos can be adapted and rewritten in the Arduino IDE, please make sure you have the right libraries for the Arduino IDE.

I set two codes, the first one will not write the “offset” values of the sensors to the EEPROM, so they will be lost after each power failure, the second code writes them to the EEPROM.
The second code also checks whether the values that are to be written to the EEPROM match the current values; if this is the case, the value is not written to protect the EEPROM.

ATTENTION!!! The EEPROM of the ESP8266 and ESP32 can only handle a maximum of 100000 write cycles, after which the EEPROM’s susceptibility to errors increases significantly.

I use DS18B20 (One Wire) as a sensor.
(I will also provide another code, which outputs all the information of the One Wire sensors and writes it to the serial output of the Arduino IDE “Is required, because One Wire sensors have addresses, which must be stored in the code”).

The One Wire sensors require a resistor between VCC and Data, I have connected mine as follows.

5VDC
1x sensor = 4.7kOhm
2x sensor = 4.7kOhm
3x sensor = 3.3kOhm
4x sensor = 3.3kOhm
5x sensor = 2.7kOhm
6x sensor = 2.7kOhm
7x sensor = 1.0kOhm
8x sensor = 1.0kOhm
9x sensor = 0.680kOhm
10x sensor = 0.680kOhm

I am currently using a 2x2x0.8mm² cable between the ESP and the last sensor, which is approx. 20m long.

The following products are used:

TTL → RS485 converter
https://www.reichelt.de/entwicklerboards-ttl-zu-rs485-max485-debo-rs485-ttl-p282703.html?PROVID=2788&gclid=Cj0KCQiAy9msBhD0ARIsANbk0A_Uh9tqncfbP8tCHY7nDxhl6_YhAsBldZsAaFKSJLqoqQBLLSK1BnMaAhQvEALw_wcB

RS485 → USB converter
https://www.reichelt.de/raspberry-pi-usb-rs485-schnittstelle-ch340c-rpi-usb-rs485-p242783.html?PROVID=2788&gclid=Cj0KCQiAy9msBhD0ARIsANbk0A_BxBypd1_VeS-KGUQZeMSJSL1gdTq_Fd_3v05rM566BdviC-57iRkaAtYWEALw_wcB

Wemos D1 mini

This is the code for 2 One Wire sensors, it must then simply be extended by the required number (adjust registers in the Arduino code and in yaml).

As soon as I have time, I’ll add the digital input and digital output to the yaml code.
It has been tested and the function is given.
If anyone is interested, please feel free to extend the yaml code and post it :wink:

The yaml code is written in the modbus.yaml, to create it, create the following line in the configuration.yaml:

modbus: !include modbus.yaml

Arduino IDE Code WITHOU describing the EEPROM

// Modbus Master Setting: Timeout: 3000ms; Polls: 350ms

// Wemos D1 mini GPIO Overview with reference to the board's printed labels
// GPIO0 = D3   Output
// GPIO1 = TX   -----               // GPIO12 = D6   Input/ Output        
// GPIO2 = D4   Output              // GPIO13 = D7   Input/ Output        
// GPIO3 = RX   -----               // GPIO14 = D5   Input/ Output        
// GPIO4 = D2   Input/ Output       // GPIO15 = D8   ----- 
// GPIO5 = D1   Input/ Output       // GPIO16 = D0   ----- 


// WARNING!!!!! Modscan increments +1 on the arrays (register addresses), meaning if I want to read array 3, I need to check Modscan under 4.
// WARNING!!!!! All data is written to the Holding Register, hence 4000X, e.g., 40004 for array 3. (Useful for CAS Modscan) in Home Assistant = input_type: holding, address: X where X = array

// Table 1.
// According to the Modbus RTU standard, the minimum silent period should be 1.75 ms regardless of the baud rate.
// Baud rate	Bit rate	Bit time	Character time	3.5 character times
// 2400	2400 bits/s	417 us	4.6 ms	16.0 ms   -> recommended: 128.0 ms
// 4800	4800 bits/s	208 us	2.3 ms	8.0 ms    -> recommended: 64.0 ms
// 9600	9600 bits/s	104 us	1.2 ms	4.0 ms    -> recommended: 32.0 ms
// 19200	19200 bits/s	52 us	573 us	2.0 ms  -> recommended: 16.0 ms
// 38400	38400 bits/s	26 us	286 us	1.75 ms (1.0 ms)
// 115200	115200 bit/s	8.7 us	95 us	1.75 ms (0.33 ms)

// Table 2.
// The number at the end defines the precision of the sensors.
// 9-bit resolution: Measures temperature in steps of 0.5 degrees Celsius.
// 10-bit resolution: Measures temperature in steps of 0.25 degrees Celsius.
// 11-bit resolution: Measures temperature in steps of 0.125 degrees Celsius.
// 12-bit resolution: Measures temperature in steps of 0.0625 degrees Celsius.

#include <ModbusRtu.h>
#include <OneWire.h>
#include <DallasTemperature.h>

#define LED 0                       // GPIO related = GPIO0 = Out
#define Switch_1 13                 // GPIO related = GPIO13 = In
#define ONE_WIRE_BUS 12             // GPIO related = GPIO5 = In
#define DEBOUNCE_TIME 75            // Time in milliseconds for debouncing related to Switch_1
#define MINIMUM_SILENT_PERIOD 16.0  // (see Table 1.) Loop delay in milliseconds, based on the minimum silent period of 16 ms for 2400 Baud! "Delay" function is not used to avoid timeouts due to blocking the entire code since the master receives no response from the slave!

unsigned long previousMillis = 0;   // Stores the time elapsed since the loop was executed. Needed for: #define MINIMUM_SILENT_PERIOD

Modbus bus;
uint16_t modbus_arry[40] = {0};     // 40 Holding Register Addresses
                                    // WARNING!!! Array position 1 is always reserved for the Modbus connection to establish... Never occupy it!!!! see   bus.poll(modbus_arry, sizeof(modbus_arry) / sizeof(modbus_arry[0]));
                                    // The count determines how many variables can be read and/or written in the registers, as well as the starting value of the register address. 
                                    // Sensor values, e.g., temperature, are transmitted as integers, and conversion is required in the Modbus master. Example: Temp: 1902 = 19.02 °C

// One Wire -> Address
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
uint8_t Temperaturprobes[7][8] = {                     // This is a two-dimensional array that stores the addresses of the temperature sensors. Each address is an array of 8 bytes (hence the [8]), and there are seven such addresses (hence the [7]).
  {0x28, 0xEA, 0xED, 0x43, 0xD4, 0xB3, 0xXX, 0x69},    // Sensor 1
  {0x28, 0x99, 0x8D, 0x75, 0xD0, 0x01, 0xXX, 0xF7},    // Sensor 2 
  {0x28, 0x79, 0x76, 0x43, 0xD4, 0x7E, 0xXX, 0xC3},    // Sensor 3
  {0x28, 0x1D, 0x1F, 0x43, 0xD4, 0xEB, 0xXX, 0x04},    // Sensor 4
  {0x28, 0xC3, 0x51, 0x43, 0xD4, 0x52, 0xXX, 0xE8},    // Sensor 5
  {0x28, 0xE3, 0xF3, 0x43, 0xD4, 0x89, 0xXX, 0x8C},    // Sensor 6
  {0x28, 0x37, 0x2D, 0x43, 0xD4, 0x32, 0xXX, 0x13}     // Sensor 7
}; 


//----------------------------------------------------------------------- MODBUS -------------------------------------------------------------------------------------------------------------

void initializeModbus() {
  bus = Modbus(1, 1, 14);          // 1 = Slave Board Address (Specifies whether this is a Slave Board, anything >0 is a Slave Board = 0 Master Board); 1 = RS485 (if 0 is used, communication is via RS232); 12 = Read/Write Pin from the "Modbus - TTL" module to ESP8266
  bus.begin(19200);                // Baud rate       
}


//----------------------------------------------------------------------- SETUP --------------------------------------------------------------------------------------------------------------

void setup() {
  initializeModbus();
  pinMode(LED, OUTPUT);
  pinMode(Switch_1, INPUT);
  sensors.begin();
  for (int i = 0; i < 7; i++) {                       // This for loop runs from 0 to 7 (total 8 loops as there are 7 sensors). In each loop, a sensor is queried and its offset applied. Index i is used to select the corresponding sensor and offset. Starts at 0, hence 0 = Sensor 1, 1 = Sensor 2, etc. must be adapted to the number of sensors (currently 7 sensors here)
    sensors.setResolution(Temperaturprobes[i], 12);   // See table above (Table 2.)
  }
}


//----------------------------------------------------------------------- LOOP ---------------------------------------------------------------------------------------------------------------

void loop() {
  unsigned long currentMillis = millis();    
  if ((currentMillis - previousMillis) >= MINIMUM_SILENT_PERIOD) {   

    static unsigned long lastDebounceTime = 0;
    static int lastSwitchState = LOW;

    bus.poll(modbus_arry, sizeof(modbus_arry) / sizeof(modbus_arry[0]));  


    //.......Outputs......................................................................
    // Array 2 LED Out
    if (modbus_arry[2] == 1) {
      digitalWrite(LED, HIGH);  // LED On if 1
    } else {
      digitalWrite(LED, LOW);   // LED OFF for any other value, including 0
    }


    //.......Inputs.......................................................................
    // Array 3 Switch_1 In with Debouncing
    int switchState = digitalRead(Switch_1);
    if (switchState != lastSwitchState) {
      lastDebounceTime = millis();
    }
    if ((millis() - lastDebounceTime) > DEBOUNCE_TIME) {
      modbus_arry[3] = switchState;
    }
    lastSwitchState = switchState;


   // One Wire - (Arry 10 up to 19 Sensor Input) and (Arry 20 up to 29 Offset Input)
    sensors.requestTemperatures();

    for (int i = 0; i < 7; i++) {                                              // (The number before the < defines the number of connected sensors) Sensor count starts at 0, so 0 = Sensor 1, 1 = Sensor 2, etc. must be adjusted to the number of sensors (currently 7 sensors here)
      float temperature = sensors.getTempC(Temperaturprobes[i]);
      if (!isnan(temperature)) {
        temperature += modbus_arry[20 + i] / 100.0;                            // (The number before the + indicates the start of the offset arrays), must be adjusted to the number of sensors!!! Array 20 writes the offset for Sensor 1 (array 4), Array 21 writes the offset for Sensor 2 (array 5), etc.
        int16_t tempInt = static_cast<int16_t>(temperature * 100); 
        modbus_arry[10 + i] = tempInt;                                         // (The number before the + indicates the start of the sensor arrays) The measured and "offset" adjusted temperature is written to modbus_arry to make it available via Modbus. 
      }
    }

    previousMillis = currentMillis;
  }
}

Arduino IDE Code WITH describing the EEPROM

// WARNING!!!!! Some parameters are written to the EEPROM; write cycles are limited to 100,000 cycles!!!!

// Modbus Master Setting: Timeout: 3000ms; Polls: 350ms

// Wemos D1 mini GPIO Overview with reference to the board's printed labels
// GPIO0 = D3   Output
// GPIO1 = TX   -----               // GPIO12 = D6   Input/ Output        
// GPIO2 = D4   Output              // GPIO13 = D7   Input/ Output        
// GPIO3 = RX   -----               // GPIO14 = D5   Input/ Output        
// GPIO4 = D2   Input/ Output       // GPIO15 = D8   ----- 
// GPIO5 = D1   Input/ Output       // GPIO16 = D0   ----- 


// WARNING!!!!! Modscan increments +1 on the arrays (register addresses), meaning if I want to read array 3, I need to check Modscan under 4.
// WARNING!!!!! All data is written to the Holding Register, hence 4000X, e.g., 40004 for array 3. (Useful for CAS Modscan) in Home Assistant = input_type: holding, address: X where X = array

// Table 1.
// According to the Modbus RTU standard, the minimum silent period should be 1.75 ms regardless of the baud rate.
// Baud rate	Bit rate	Bit time	Character time	3.5 character times
// 2400	2400 bits/s	417 us	4.6 ms	16.0 ms   -> recommended: 128.0 ms
// 4800	4800 bits/s	208 us	2.3 ms	8.0 ms    -> recommended: 64.0 ms
// 9600	9600 bits/s	104 us	1.2 ms	4.0 ms    -> recommended: 32.0 ms
// 19200	19200 bits/s	52 us	573 us	2.0 ms  -> recommended: 16.0 ms
// 38400	38400 bits/s	26 us	286 us	1.75 ms (1.0 ms)
// 115200	115200 bit/s	8.7 us	95 us	1.75 ms (0.33 ms)

// Table 2.
// The number at the end defines the precision of the sensors.
// 9-bit resolution: Measures temperature in steps of 0.5 degrees Celsius.
// 10-bit resolution: Measures temperature in steps of 0.25 degrees Celsius.
// 11-bit resolution: Measures temperature in steps of 0.125 degrees Celsius.
// 12-bit resolution: Measures temperature in steps of 0.0625 degrees Celsius.

#include <ModbusRtu.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <EEPROM.h>

#define LED 0                         // GPIO reference = GPIO0 = Out
#define Switch_1 13                   // GPIO reference = GPIO13 = In
#define ONE_WIRE_BUS 12               // GPIO reference = GPIO12 = In
#define DEBOUNCE_TIME 75              // Time in milliseconds for debouncing related to Switch_1
#define MINIMUM_SILENT_PERIOD 16.0    // (see Table 1.) Loop delay in milliseconds, based on the minimum silent period of 16 ms for 2400 Baud! "Delay" function is not used as it leads to timeouts by blocking the entire code, as the Master doesn't receive a response from the Slave!

unsigned long previousMillis = 0;     // Stores the time elapsed since the loop was executed. Needed for: #define MINIMUM_SILENT_PERIOD

Modbus bus;
uint16_t modbus_arry[40] = {0};       // 40 Holding Register Addresses
                                      // WARNING!!! Array position 1 is always reserved for the Modbus connection to establish... Never occupy it!!!! see   bus.poll(modbus_arry, sizeof(modbus_arry) / sizeof(modbus_arry[0]));
                                      // The count determines how many variables can be read and/or written in the registers, as well as the starting value of the register address. 
                                      // Sensor values, e.g., temperature, are transmitted as integers, and conversion is required in the Modbus master. Example: Temp: 1902 = 19.02 °C

// One Wire -> Address
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
uint8_t Temperaturprobes[7][8] = {                     // This is a two-dimensional array that stores the addresses of temperature sensors. Each address is an array of 8 bytes (hence the [8]), and there are four such addresses (hence the [7]).
  {0x28, 0xEA, 0xED, 0x43, 0xD4, 0xB3, 0xXX, 0x69},    // Sensor 1
  {0x28, 0x99, 0x8D, 0x75, 0xD0, 0x01, 0xXX, 0xF7},    // Sensor 2 
  {0x28, 0x79, 0x76, 0x43, 0xD4, 0x7E, 0xXX, 0xC3},    // Sensor 3
  {0x28, 0x1D, 0x1F, 0x43, 0xD4, 0xEB, 0xXX, 0x04},    // Sensor 4
  {0x28, 0xC3, 0x51, 0x43, 0xD4, 0x52, 0xXX, 0xE8},    // Sensor 5
  {0x28, 0xE3, 0xF3, 0x43, 0xD4, 0x89, 0xXX, 0x8C},    // Sensor 6
  {0x28, 0x37, 0x2D, 0x43, 0xD4, 0x32, 0xXX, 0x13}     // Sensor 7
}; 


//----------------------------------------------------------------------- MODBUS -------------------------------------------------------------------------------------------------------------

void initializeModbus() {
  bus = Modbus(1, 1, 14);          // 1 = Slave Board Address (Specifies whether this is a Slave Board; anything > 0 is a Slave Board = 0 Master Board); 1 = RS485 (if 0 is used, communication is via RS232); 12 = Read/Write Pin from "Modbus - TTL" module to ESP8266
  bus.begin(19200);                // Baud rate       
}


//----------------------------------------------------------------------- EEPROM -------------------------------------------------------------------------------------------------------------
// Function to write values to EEPROM
void writeToEEPROM() {
  for (int i = 20; i < 30; i++) {  // i = X, where X is replaced by the Arry (Register) from which reading or writing to EEPROM should start. < X, X is replaced by the Arry (Register) at which reading or EEPROM writing ends.
    uint16_t oldValue;
    EEPROM.get(i * sizeof(uint16_t), oldValue);
    if (oldValue != modbus_arry[i]) {
      EEPROM.put(i * sizeof(uint16_t), modbus_arry[i]);
    }
  }
}


//----------------------------------------------------------------------- SETUP --------------------------------------------------------------------------------------------------------------

void setup() {
  initializeModbus();
  pinMode(LED, OUTPUT);
  pinMode(Switch_1, INPUT);
  sensors.begin();
  for (int i = 0; i < 7; i++) {                       // This for loop runs from 0 to 7 (total 8 loops as there are 7 sensors). In each loop, a sensor is queried and its offset applied. Index i is used to select the corresponding sensor and offset. Starts at 0, hence 0 = Sensor 1, 1 = Sensor 2, etc. must be adapted to the number of sensors (currently 7 sensors here)
    sensors.setResolution(Temperaturprobes[i], 12);   // See table above (Table 2.)
  }

  // Read values from EEPROM
  for (int i = 20; i < 30; i++) {                     // i = X, where X is replaced by the Arry (Register) from which reading or writing to EEPROM should start. < X, X is replaced by the Arry (Register) at which reading or EEPROM writing ends.
    uint16_t value;
    EEPROM.get(i * sizeof(uint16_t), value);
    modbus_arry[i] = value;
  }
}


//----------------------------------------------------------------------- LOOP ---------------------------------------------------------------------------------------------------------------

void loop() {
  unsigned long currentMillis = millis();    
  if ((currentMillis - previousMillis) >= MINIMUM_SILENT_PERIOD) {   

    static unsigned long lastDebounceTime = 0;
    static int lastSwitchState = LOW;

    bus.poll(modbus_arry, sizeof(modbus_arry) / sizeof(modbus_arry[0]));  


    //.......Outputs......................................................................
    // Arry 2 LED Out
    if (modbus_arry[2] == 1) {
      digitalWrite(LED, HIGH);  // LED On if 1
    } else {
      digitalWrite(LED, LOW);   // LED OFF for any other value, including 0
    }


    //.......Inputs.......................................................................
    // Arry 3 Switch_1 In with Debouncing
    int switchState = digitalRead(Switch_1);
    if (switchState != lastSwitchState) {
      lastDebounceTime = millis();
    }
    if ((millis() - lastDebounceTime) > DEBOUNCE_TIME) {
      modbus_arry[3] = switchState;
    }
    lastSwitchState = switchState;


   // One Wire - (Arry 10 up to 19 Sensor Input) and (Arry 20 up to 29 Offset Input)
    sensors.requestTemperatures(); // Move this call here

    for (int i = 0; i < 7; i++) {                                              // (The number before the < defines the number of connected sensors) Sensor count starts at 0, so 0 = Sensor 1, 1 = Sensor 2, etc. must be adjusted to the number of sensors (currently 7 sensors here)
      float temperature = sensors.getTempC(Temperaturprobes[i]);
      if (!isnan(temperature)) {
        temperature += modbus_arry[20 + i] / 100.0;                            // (The number before the + indicates the start of the offset arrays), must be adjusted to the number of sensors!!! Array 20 writes the offset for Sensor 1 (array 4), Array 21 writes the offset for Sensor 2 (array 5), etc.
        int16_t tempInt = static_cast<int16_t>(temperature * 100); 
        modbus_arry[10 + i] = tempInt;                                         // (The number before the + indicates the start of the sensor arrays) The measured and "offset" adjusted temperature is written to modbus_arry to make it available via Modbus. 
      }
    }

    // Check if values have changed and write them to EEPROM if necessary
    writeToEEPROM();

    previousMillis = currentMillis;
  }
}

Home Assistant modbus.yaml Code:



Modbus RTU Verbindung mit Wemos D1 mini als Slave
- name: Modbus_Hub
  type: serial
  port: /dev/ttyUSB0
  baudrate: 19200
  bytesize: 8
  method: rtu
  parity: N
  stopbits: 1
  delay: 4   #in Seconds, Time to delay sending messages in seconds after connecting.
  message_wait_milliseconds: 65 #Time to wait in milliseconds between requests. Default: 30 for serial connection, 0 for all other connections.
  timeout: 5 #Default 5, Timeout while waiting for a response in seconds.
  sensors:
    - name: Test_Input10
      input_type: holding
      address: 10
      slave: 1
      unique_id: Test_Input10
      unit_of_measurement: °C
      scale: 0.01
      offset: 0
      precision: 2
    - name: Test_Input11
      input_type: holding
      address: 11
      slave: 1
      unique_id: Test_Input11
      unit_of_measurement: °C
      scale: 0.01
      offset: 0
      precision: 2
    - name: Test_Input12
      input_type: holding
      address: 12
      slave: 1
      unique_id: Test_Input12
      unit_of_measurement: °C
      scale: 0.01
      offset: 0
      precision: 2
    - name: Test_Input13
      input_type: holding
      address: 13
      slave: 1
      unique_id: Test_Input13
      unit_of_measurement: °C
      scale: 0.01
      offset: 0
      precision: 2
    - name: Test_Input14
      input_type: holding
      address: 14
      slave: 1
      unique_id: Test_Input14
      unit_of_measurement: °C
      scale: 0.01
      offset: 0
      precision: 2
    - name: Test_Input15
      input_type: holding
      address: 15
      slave: 1
      unique_id: Test_Input15
      unit_of_measurement: °C
      scale: 0.01
      offset: 0
      precision: 2
    - name: Test_Input16
      input_type: holding
      address: 16
      slave: 1
      unique_id: Test_Input16
      unit_of_measurement: °C
      scale: 0.01
      offset: 0
      precision: 2
  binary_sensors:
    - name: Test_Binary_Input1
      input_type: holding
      address: 3
      slave: 1
      unique_id: Test_Binary_Input1
      device_class: running
      scan_interval: 60   #Update interval in seconds. scan_interval = 0 for no polling. Entities are unavailable until the first scan interval is passed, except for entities with scan_interval = 0, which are read at startup and not updated.
      slave_count: 0      #Generate count+1 binary sensors (master + slaves). Addresses are automatically incremented. The parameter simplifies configuration and provides a much better performance by not using count+1 requests but a single request.
  switches:
    - name: Test_Binary_Output1
      address: 2
      slave: 1
      write_type: holding
      command_on: 1
      command_off: 0
      verify:
          delay: 2        #Seconds
          input_type: holding
          address: 2
          state_on: 1
          state_off: 0