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:
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
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