6.2.1.16 Add TasmotaModbus lib

6.2.1.16 20181015
 * Add TasmotaModbus library for very basic modbus wrapper for TasmotaSerial
 * Change xsns_17_senseair.ino to use TasmotaModbus library
 * Fix xnrg_05_pzem2.ino for PZEM-014/016 support using TasmotaModbus library (#3694)
This commit is contained in:
Theo Arends 2018-10-15 17:18:30 +02:00
parent 6b660026cd
commit fb6cc194a9
11 changed files with 336 additions and 174 deletions

View File

@ -0,0 +1,7 @@
# TasmotaSerial
Implementation of software serial with hardware serial fallback library for the ESP8266
Allows for several instances to be active at the same time.
Please note that due to the fact that the ESP always have other activities ongoing, there will be some inexactness in interrupt timings. This may lead to bit errors when having heavy data traffic.

View File

@ -0,0 +1,31 @@
#include <TasmotaModbus.h>
TasmotaModbus Modbus(14, 12);
void setup() {
Serial.begin(115200);
Modbus.Begin(9600);
Serial.println("\nTasmotaModbus test started");
Modbus.Send(0x01, 0x04, 0, 8);
}
void loop() {
if (Modbus.ReceiveReady()) {
uint8_t buffer[26];
uint8_t error = Modbus.ReceiveBuffer(buffer, 8);
if (error) {
Serial.print("Modbus response error ");
Serial.println(error);
} else {
Serial.print("Modbus received:");
for (int i = 0; i < (buffer[2]) ? buffer[2] +5 : sizeof(buffer); i++) {
Serial.print(" ");
Serial.print(buffer[i], HEX);
}
}
}
}

View File

@ -0,0 +1,26 @@
#######################################
# Syntax Coloring Map for TasmotaModbus
# (esp8266)
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
TasmotaModbus KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
Begin KEYWORD2
Send KEYWORD2
ReceiveReady KEYWORD2
ReceiveBuffer KEYWORD2
Receive16BitRegister KEYWORD2
Receive32BitRegister KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################

View File

@ -0,0 +1,15 @@
{
"name": "TasmotaModbus",
"version": "1.0.0",
"keywords": [
"serial", "io", "TasmotaModbus"
],
"description": "Basic modbus wrapper for TasmotaSerial for ESP8266.",
"repository":
{
"type": "git",
"url": "https://github.com/arendst/Sonoff-Tasmota/lib/TasmotaModbus"
},
"frameworks": "arduino",
"platforms": "espressif8266"
}

View File

@ -0,0 +1,9 @@
name=TasmotaModbus
version=1.0.0
author=Theo Arends
maintainer=Theo Arends <theo@arends.com>
sentence=Basic modbus wrapper for TasmotaSerial for ESP8266.
paragraph=
category=Signal Input/Output
url=
architectures=esp8266

View File

@ -0,0 +1,141 @@
/*
TasmotaModbus.cpp - Basic modbus wrapper for TasmotaSerial for Tasmota
Copyright (C) 2018 Theo Arends
This library is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TasmotaModbus.h"
TasmotaModbus::TasmotaModbus(int receive_pin, int transmit_pin) : TasmotaSerial(receive_pin, transmit_pin, 1)
{
}
TasmotaModbus::~TasmotaModbus()
{
}
uint16_t CalculateCRC(uint8_t *frame, uint8_t num)
{
uint16_t crc = 0xFFFF;
uint16_t flag;
for (uint8_t i = 0; i < num; i++) {
crc ^= frame[i];
for (uint8_t j = 8; j; j--) {
if ((crc & 0x0001) != 0) { // If the LSB is set
crc >>= 1; // Shift right and XOR 0xA001
crc ^= 0xA001;
} else { // Else LSB is not set
crc >>= 1; // Just shift right
}
}
}
return crc;
}
int TasmotaModbus::Begin(long speed)
{
int result = 0;
if (begin(speed)) {
result = 1;
if (hardwareSerial()) { result = 2; }
}
return result;
}
void TasmotaModbus::Send(uint8_t device_address, uint8_t function_code, uint16_t start_address, uint16_t register_count)
{
uint8_t frame[8];
frame[0] = device_address; // 0xFE default device address or dedicated like 0x01
frame[1] = function_code;
frame[2] = (uint8_t)(start_address >> 8);
frame[3] = (uint8_t)(start_address);
frame[4] = (uint8_t)(register_count >> 8);
frame[5] = (uint8_t)(register_count);
uint16_t crc = CalculateCRC(frame, 6);
frame[6] = (uint8_t)(crc);
frame[7] = (uint8_t)(crc >> 8);
flush();
write(frame, sizeof(frame));
}
bool TasmotaModbus::ReceiveReady()
{
return (available() > 4);
}
uint8_t TasmotaModbus::ReceiveBuffer(uint8_t *buffer, uint8_t register_count)
{
uint8_t len = 0;
uint32_t last = millis();
while ((available() > 0) && (len < (register_count *2) + 5) && (millis() - last < 10)) {
buffer[len++] = (uint8_t)read();
if (3 == len) {
if (buffer[1] & 0x80) { // fe 84 02 f2 f1
return buffer[2]; // 1 = Illegal Function, 2 = Illegal Address, 3 = Illegal Data, 4 = Slave Error
}
}
last = millis();
}
if (len < 7) { return 7; } // 7 = Not enough data
if (len != buffer[2] + 5) { return 8; } // 8 = Unexpected result
uint16_t crc = (buffer[len -1] << 8) | buffer[len -2];
if (CalculateCRC(buffer, len -2) != crc) { return 9; } // 9 = crc error
return 0; // 0 = No error
}
uint8_t TasmotaModbus::Receive16BitRegister(uint16_t *value)
{
// 0 1 2 3 4 5 6
// 01 04 02 43 21 HH LL
// Id Cc Sz Regis Crc--
uint8_t buffer[7];
uint8_t error = ReceiveBuffer(buffer, 1); // 1 x 16bit register
if (!error) {
*value = (buffer[3] << 8) | buffer[4];
}
return error;
}
uint8_t TasmotaModbus::Receive32BitRegister(float *value)
{
// 0 1 2 3 4 5 6 7 8
// 01 04 04 87 65 43 21 HH LL
// Id Cc Sz Register--- Crc--
uint8_t buffer[9];
*value = NAN;
uint8_t error = ReceiveBuffer(buffer, 2); // 1 x 32bit register
if (!error) {
((uint8_t*)value)[3] = buffer[3];
((uint8_t*)value)[2] = buffer[4];
((uint8_t*)value)[1] = buffer[5];
((uint8_t*)value)[0] = buffer[6];
}
return error;
}

View File

@ -0,0 +1,54 @@
/*
TasmotaModbus.h - Basic modbus wrapper for TasmotaSerial for Tasmota
Copyright (C) 2018 Theo Arends
This library is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TasmotaModbus_h
#define TasmotaModbus_h
#include <Arduino.h>
#include <TasmotaSerial.h>
#define TM_MODBUS_BAUDRATE 9600 // Default baudrate
class TasmotaModbus : public TasmotaSerial {
public:
TasmotaModbus(int receive_pin, int transmit_pin);
~TasmotaModbus();
int Begin(long speed = TM_MODBUS_BAUDRATE);
void Send(uint8_t device_address, uint8_t function_code, uint16_t start_address, uint16_t register_count);
bool ReceiveReady();
/* Return codes:
* 0 - No error
* 1 - Illegal function
* 2 - Illegal address
* 3 - Illegal data
* 4 - Slave error
* 7 - Not enough minimal data received
* 8 - Not enough data receieved
* 9 - Crc error
*/
uint8_t ReceiveBuffer(uint8_t *buffer, uint8_t register_count);
uint8_t Receive16BitRegister(uint16_t *value);
uint8_t Receive32BitRegister(float *value);
};
#endif // TasmotaModbus_h

View File

@ -1,4 +1,9 @@
/* 6.2.1.15 20181012
/* 6.2.1.16 20181015
* Add TasmotaModbus library for very basic modbus wrapper for TasmotaSerial
* Change xsns_17_senseair.ino to use TasmotaModbus library
* Fix xnrg_05_pzem2.ino for PZEM-014/016 support using TasmotaModbus library (#3694)
*
* 6.2.1.15 20181012
* Fix Color Temperature slider functionality regression from 6.2.1.5 (#4037)
* Add auto reload of main web page to some web restarts
* Add whitespace removal from RfRaw and SerialSend5 (#4020)

View File

@ -20,7 +20,7 @@
#ifndef _SONOFF_VERSION_H_
#define _SONOFF_VERSION_H_
#define VERSION 0x0602010F
#define VERSION 0x06020110
#define D_PROGRAMNAME "Sonoff-Tasmota"
#define D_AUTHOR "Theo Arends"

View File

@ -34,108 +34,40 @@
#define XNRG_05 5
#define PZEM2_TYPES_003_017 8 // Result 16 bit register count
#define PZEM2_TYPES_014_016 10 // Result 16 bit register count
#define PZEM2_MODBUS_SPEED 9600
#define PZEM2_DEVICE_ADDRESS 0x01 // PZEM default address
#define PZEM2_READ_RESULT 0x04 // Command Read result
#define PZEM2_READ_RESULT 0x04
#define PZEM2_TYPES_003_017 8 // Result 8 x 16 bit register count
#define PZEM2_TYPES_014_016 10 // Result 10 x 16 bit register count
#include <TasmotaSerial.h>
TasmotaSerial *Pzem2Serial;
#include <TasmotaModbus.h>
TasmotaModbus *Pzem2Modbus;
uint8_t pzem2_type = PZEM2_TYPES_014_016;
/*********************************************************************************************/
uint16_t Pzem2ModbusCalculateCRC(uint8_t *frame, uint8_t num)
{
uint16_t crc = 0xFFFF;
uint16_t flag;
for (uint8_t i = 0; i < num; i++) {
crc ^= frame[i];
for (uint8_t j = 8; j; j--) {
if ((crc & 0x0001) != 0) { // If the LSB is set
crc >>= 1; // Shift right and XOR 0xA001
crc ^= 0xA001;
} else { // Else LSB is not set
crc >>= 1; // Just shift right
}
}
}
return crc;
}
void Pzem2ModbusSend(uint8_t function_code, uint16_t start_address, uint16_t register_count)
{
uint8_t frame[8];
frame[0] = 0xFE; // Any Address
frame[1] = function_code;
frame[2] = (uint8_t)(start_address >> 8);
frame[3] = (uint8_t)(start_address);
frame[4] = (uint8_t)(register_count >> 8);
frame[5] = (uint8_t)(register_count);
uint16_t crc = Pzem2ModbusCalculateCRC(frame, 6);
frame[6] = (uint8_t)((crc >> 8) & 0xFF);
frame[7] = (uint8_t)(crc & 0xFF);
Pzem2Serial->flush();
Pzem2Serial->write(frame, sizeof(frame));
}
bool Pzem2ModbusReceiveReady()
{
return (Pzem2Serial->available() >= 5); // 5 - Error frame, 21 or 25 - Ok frame
}
uint8_t Pzem2ModbusReceive(uint8_t *buffer, uint8_t register_count)
{
// 0 1 2 3 4 5 6
// FE 04 02 08 98 HH LL
// Id Cc Sz Regis Crc--
uint8_t len = 0;
while ((Pzem2Serial->available() > 0) && (len < (register_count *2) + 5)) {
buffer[len++] = (uint8_t)Pzem2Serial->read();
if (3 == len) {
if (buffer[1] & 0x80) { // fe 84 02 f2 f1
return buffer[2]; // 1 = Illegal Function, 2 = Illegal Address, 3 = Illegal Data, 4 = Slave Error
}
}
}
AddLogSerial(LOG_LEVEL_DEBUG_MORE, buffer, len);
if (len < 7) { return 7; } // 7 = Not enough data
if (len != buffer[2] + 5) { return 8; } // 8 = Unexpected result
uint16_t crc = (buffer[len -2] << 8) | buffer[len -1];
if (Pzem2ModbusCalculateCRC(buffer, len -3) != crc) { return 9; } // 9 = crc error
return 0; // 0 = No error
}
/*********************************************************************************************/
uint8_t pzem2_sendRetry = 0;
void Pzem2Every200ms()
void Pzem2EverySecond()
{
bool data_ready = Pzem2ModbusReceiveReady();
bool data_ready = Pzem2Modbus->ReceiveReady();
if (data_ready) {
uint8_t buffer[26];
uint8_t error = Pzem2ModbusReceive(buffer, pzem2_type);
uint8_t error = Pzem2Modbus->ReceiveBuffer(buffer, pzem2_type);
AddLogSerial(LOG_LEVEL_DEBUG_MORE, buffer, (buffer[2]) ? buffer[2] +5 : sizeof(buffer));
if (error) {
snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_DEBUG "PZEM2 response error %d"), error);
AddLog(LOG_LEVEL_DEBUG);
// if (9 == error) {
/*
if (PZEM2_TYPES_014_016 == pzem2_type) {
pzem2_type = PZEM2_TYPES_003_017;
} else {
pzem2_type = PZEM2_TYPES_014_016;
}
*/
// }
} else {
float energy = 0;
@ -143,12 +75,12 @@ void Pzem2Every200ms()
if (PZEM2_TYPES_003_017 == pzem2_type) {
energy_type_dc = true;
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// FE 04 10 27 10 00 64 03 E8 00 00 00 00 00 00 00 00 00 00 HH LL = PZEM-017
// 01 04 10 27 10 00 64 03 E8 00 00 00 00 00 00 00 00 00 00 HH LL = PZEM-017
// Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc--
energy_voltage = (float)((buffer[3] << 8) + buffer[4]) / 100.0; // 655.00 V
energy_current = (float)((buffer[5] << 8) + buffer[6]) / 100.0; // 655.00 A
energy_active_power = (float)((uint32_t)buffer[9] << 24 + (uint32_t)buffer[10] << 16 + (uint32_t)buffer[7] << 8 + buffer[8]) / 10.0; // 429496729.0 W
energy = (float)((uint32_t)buffer[13] << 24 + (uint32_t)buffer[14] << 16 + (uint32_t)buffer[11] << 8 + buffer[12]); // 4294967295 Wh
energy_voltage = (float)((buffer[3] << 8) + buffer[4]) / 100.0; // 655.00 V
energy_current = (float)((buffer[5] << 8) + buffer[6]) / 100.0; // 655.00 A
energy_active_power = (float)((buffer[9] << 24) + (buffer[10] << 16) + (buffer[7] << 8) + buffer[8]) / 10.0; // 429496729.0 W
energy = (float)((buffer[13] << 24) + (buffer[14] << 16) + (buffer[11] << 8) + buffer[12]); // 4294967295 Wh
if (!energy_start || (energy < energy_start)) { energy_start = energy; } // Init after restart and hanlde roll-over if any
energy_kWhtoday += (energy - energy_start) * 100;
energy_start = energy;
@ -157,14 +89,14 @@ void Pzem2Every200ms()
else if (PZEM2_TYPES_014_016 == pzem2_type) { // PZEM-014,016
energy_type_dc = false;
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// FE 04 14 08 98 03 E8 00 00 08 98 00 00 00 00 00 00 01 F4 00 64 00 00 HH LL = PZEM-014
// 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34 = PZEM-014
// Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc--
energy_voltage = (float)((buffer[3] << 8) + buffer[4]) / 10.0; // 6553.0 V
energy_current = (float)((uint32_t)buffer[7] << 24 + (uint32_t)buffer[8] << 16 + (uint32_t)buffer[5] << 8 + buffer[6]) / 1000.0; // 4294967.000 A
energy_active_power = (float)((uint32_t)buffer[11] << 24 + (uint32_t)buffer[12] << 16 + (uint32_t)buffer[9] << 8 + buffer[10]) / 10.0; // 429496729.0 W
energy_frequency = (float)((buffer[17] << 8) + buffer[18]) / 10.0; // 50.0 Hz
energy_power_factor = (float)((buffer[19] << 8) + buffer[20]) / 100.0; // 1.00
energy = (float)((uint32_t)buffer[15] << 24 + (uint32_t)buffer[16] << 16 + (uint32_t)buffer[13] << 8 + buffer[14]); // 4294967295 Wh
energy_voltage = (float)((buffer[3] << 8) + buffer[4]) / 10.0; // 6553.0 V
energy_current = (float)((buffer[7] << 24) + (buffer[8] << 16) + (buffer[5] << 8) + buffer[6]) / 1000.0; // 4294967.000 A
energy_active_power = (float)((buffer[11] << 24) + (buffer[12] << 16) + (buffer[9] << 8) + buffer[10]) / 10.0; // 429496729.0 W
energy_frequency = (float)((buffer[17] << 8) + buffer[18]) / 10.0; // 50.0 Hz
energy_power_factor = (float)((buffer[19] << 8) + buffer[20]) / 100.0; // 1.00
energy = (float)((buffer[15] << 24) + (buffer[16] << 16) + (buffer[13] << 8) + buffer[14]); // 4294967295 Wh
if (!energy_start || (energy < energy_start)) { energy_start = energy; } // Init after restart and hanlde roll-over if any
energy_kWhtoday += (energy - energy_start) * 100;
energy_start = energy;
@ -175,7 +107,7 @@ void Pzem2Every200ms()
if (0 == pzem2_sendRetry || data_ready) {
pzem2_sendRetry = 5;
Pzem2ModbusSend(PZEM2_READ_RESULT, 0, pzem2_type);
Pzem2Modbus->Send(PZEM2_DEVICE_ADDRESS, PZEM2_READ_RESULT, 0, pzem2_type);
}
else {
pzem2_sendRetry--;
@ -184,10 +116,10 @@ void Pzem2Every200ms()
void Pzem2SnsInit()
{
// Software serial init needs to be done here as earlier (serial) interrupts may lead to Exceptions
Pzem2Serial = new TasmotaSerial(pin[GPIO_PZEM2_RX], pin[GPIO_PZEM2_TX], 1);
if (Pzem2Serial->begin(9600)) {
if (Pzem2Serial->hardwareSerial()) { ClaimSerial(); }
Pzem2Modbus = new TasmotaModbus(pin[GPIO_PZEM2_RX], pin[GPIO_PZEM2_TX]);
uint8_t result = Pzem2Modbus->Begin(PZEM2_MODBUS_SPEED);
if (result) {
if (2 == result) { ClaimSerial(); }
} else {
energy_flg = ENERGY_NONE;
}
@ -218,8 +150,8 @@ int Xnrg05(byte function)
case FUNC_INIT:
Pzem2SnsInit();
break;
case FUNC_EVERY_200_MSECOND:
Pzem2Every200ms();
case FUNC_EVERY_SECOND:
Pzem2EverySecond();
break;
}
}

View File

@ -26,7 +26,9 @@
* Hardware Serial will be selected if GPIO1 = [SAir Rx] and GPIO3 = [SAir Tx]
\*********************************************************************************************/
#include <TasmotaSerial.h>
#define SENSEAIR_MODBUS_SPEED 9600
#define SENSEAIR_DEVICE_ADDRESS 0xFE // Any address
#define SENSEAIR_READ_REGISTER 0x04 // Command Read
#ifndef CO2_LOW
#define CO2_LOW 800 // Below this CO2 value show green light
@ -35,7 +37,8 @@
#define CO2_HIGH 1200 // Above this CO2 value show red light
#endif
TasmotaSerial *SensairSerial;
#include <TasmotaModbus.h>
TasmotaModbus *SenseairModbus;
const char kSenseairTypes[] PROGMEM = "Kx0|S8";
@ -48,68 +51,6 @@ float senseair_humidity = 0;
//uint8_t senseair_state = 0;
/*********************************************************************************************/
void ModbusSend(uint8_t function_code, uint16_t start_address, uint16_t register_count)
{
uint8_t frame[8];
frame[0] = 0xFE; // Any Address
frame[1] = function_code;
frame[2] = (uint8_t)(start_address >> 8);
frame[3] = (uint8_t)(start_address);
frame[4] = (uint8_t)(register_count >> 8);
frame[5] = (uint8_t)(register_count);
uint16_t crc = 0xFFFF;
for (uint8_t pos = 0; pos < sizeof(frame) -2; pos++) {
crc ^= (uint16_t)frame[pos]; // XOR byte into least sig. byte of crc
for (uint8_t i = 8; i != 0; i--) { // Loop over each bit
if ((crc & 0x0001) != 0) { // If the LSB is set
crc >>= 1; // Shift right and XOR 0xA001
crc ^= 0xA001;
}
else { // Else LSB is not set
crc >>= 1; // Just shift right
}
}
}
frame[7] = (uint8_t)((crc >> 8) & 0xFF);
frame[6] = (uint8_t)(crc & 0xFF);
SensairSerial->flush();
SensairSerial->write(frame, sizeof(frame));
}
bool ModbusReceiveReady()
{
return (SensairSerial->available() >= 5); // 5 - Error frame, 7 - Ok frame
}
uint8_t ModbusReceive(uint16_t *value)
{
uint8_t buffer[7];
uint8_t len = 0;
while (SensairSerial->available() > 0) {
buffer[len++] = (uint8_t)SensairSerial->read();
if (3 == len) {
if (buffer[1] & 0x80) { // fe 84 02 f2 f1
return buffer[2]; // 1 = Illegal Function, 2 = Illegal Data Address, 3 = Illegal Data Value
}
}
}
AddLogSerial(LOG_LEVEL_DEBUG_MORE, buffer, len);
if (len != sizeof(buffer)) {
return 9; // 9 = Unexpected result
}
*value = (buffer[3] << 8) | buffer[4];
return 0; // 0 = No error
}
/*********************************************************************************************/
const uint8_t start_addresses[] { 0x1A, 0x00, 0x03, 0x04, 0x05, 0x1C, 0x0A };
uint8_t senseair_read_state = 0;
@ -122,10 +63,10 @@ void Senseair250ms() // Every 250 mSec
// senseair_state = 0;
uint16_t value = 0;
bool data_ready = ModbusReceiveReady();
bool data_ready = SenseairModbus->ReceiveReady();
if (data_ready) {
uint8_t error = ModbusReceive(&value);
uint8_t error = SenseairModbus->Receive16BitRegister(&value);
if (error) {
snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_DEBUG "SenseAir response error %d"), error);
AddLog(LOG_LEVEL_DEBUG);
@ -179,7 +120,7 @@ void Senseair250ms() // Every 250 mSec
if (0 == senseair_send_retry || data_ready) {
senseair_send_retry = 5;
ModbusSend(0x04, (uint16_t)start_addresses[senseair_read_state], 1);
SenseairModbus->Send(SENSEAIR_DEVICE_ADDRESS, SENSEAIR_READ_REGISTER, (uint16_t)start_addresses[senseair_read_state], 1);
} else {
senseair_send_retry--;
}
@ -193,9 +134,10 @@ void SenseairInit()
{
senseair_type = 0;
if ((pin[GPIO_SAIR_RX] < 99) && (pin[GPIO_SAIR_TX] < 99)) {
SensairSerial = new TasmotaSerial(pin[GPIO_SAIR_RX], pin[GPIO_SAIR_TX], 1);
if (SensairSerial->begin(9600)) {
if (SensairSerial->hardwareSerial()) { ClaimSerial(); }
SenseairModbus = new TasmotaModbus(pin[GPIO_SAIR_RX], pin[GPIO_SAIR_TX]);
uint8_t result = SenseairModbus->Begin(SENSEAIR_MODBUS_SPEED);
if (result) {
if (2 == result) { ClaimSerial(); }
senseair_type = 1;
}
}