ESP32 communication Fronius Primo Inverter via ModbusRTU over RS-485

Hello everyone, hope this comment finds you keeping well. Currently, I'm working on a project to predict the power output from a photovoltaic plant. Thus, I need to read the power from a Fronius Primo 3.0 inverter. For that, I'm using the ESP32 DEVKIT V1 and the MAX3485 module. In the inverter, the configuration the DATCOM status is interface with 9600 baud rate and the serial configuration on the ESP32 is 9600 bauds with 8N1 configuration.

The Modbus configuration is the next one (screenshot from the inverter PA web). Sorry about the spanish language but I supose you can guess by the numbers which field is each one:


About the Fronius String direction compensation is 100 not 101. I've tried everything that has pop up to my mind but idk I still can't read nothing from the inverter. I also leave you the hardware connections and the code I'm running at the moment.

#include <Arduino.h>
#include <ModbusMaster.h>
#include <Wire.h>

// ── Configuracion Modbus ──────────────────────────────────────
#define MODBUS_ID          100        // Cambiar a 100 si no responde
#define PIN_RX2            16
#define PIN_TX2            17
#define MODBUS_BAUD        9600UL

// ── Timing ───────────────────────────────────────────────────
#define POLL_INTERVAL_MS   5000UL
#define MODBUS_TIMEOUT_MS  1000UL
#define MAX_RETRIES        3

// ── Registros Modbus (base-0 = mapa Fronius - 1) ─────────────
#define REG_MPPT_SF_BASE   40265    // DCA_SF, DCV_SF, DCW_SF
#define REG_MPPT1_BASE     40282    // I, V, P string 1
#define REG_MPPT2_BASE     40302    // I, V, P string 2
#define REG_AC_BASE        40071    // Bloque AC completo (47 regs)
#define REG_AC_COUNT       47

// Offsets en el buffer del bloque AC (en registros de 16-bit)
// offset = fronius_reg - 40072
#define OFF_AC_CURRENT      0       // 40072-40073  A       float32
#define OFF_AC_VOLTAGE     14       // 40086-40087  PhVphA  float32
#define OFF_AC_POWER       20       // 40092-40093  W       float32
#define OFF_STATE          46       // 40118        St      uint16

// ── OLED SSD1306 128x64 ───────────────────────────────────────
#define OLED_WIDTH         128
#define OLED_HEIGHT         64
#define OLED_ADDR          0x3C
#define OLED_RESET          -1

// ── Objetos globales ──────────────────────────────────────────
ModbusMaster modbus;

// ── Estructura de datos del inversor ─────────────────────────
struct InverterData {
    float    Idc1, Vdc1, Pdc1;    // DC string 1 [A, V, W]
    float    Idc2, Vdc2, Pdc2;    // DC string 2 [A, V, W]
    float    PdcTotal;             // Potencia DC total [W]
    float    Iac, Vac, Pac;        // AC [A, V, W]
    uint16_t state;                // Estado SunSpec enum16
    bool     valid;                // Ciclo de lectura exitoso
};

InverterData inv = {};

// Scale factors Multiple MPPT (int16, sunsssf)
// Valor por defecto -2 -> x0.01 (confirmado en TFG y mapa regs)
int16_t sf_DCA = -2;
int16_t sf_DCV = -2;
int16_t sf_DCW = -2;

// ── Prototipos ────────────────────────────────────────────────
float       applyScaleFactor(uint16_t raw, int16_t sf);
float       regsToFloat(uint16_t hi, uint16_t lo);
bool        readMPPTScaleFactors();
bool        readMPPTString(uint8_t str, float &I, float &V, float &P);
bool        readACBlock();
void        runReadCycle();
//void        updateOLED();
void        printDebug();
void        preTransmission();
void        postTransmission();
const char* stateStr(uint16_t st);

// =============================================================
void setup() {

    Serial.begin(115200); 

    // 1. Iniciamos UART2 (Modbus) con sus pines (16 y 17)
    Serial2.begin(MODBUS_BAUD, SERIAL_8N1, PIN_RX2, PIN_TX2);

    // 2. Aplicamos el timeout a Serial2 (MUY IMPORTANTE)
    //Serial2.setTimeout(MODBUS_TIMEOUT_MS);

    // 3. Vinculamos la librería ModbusMaster a Serial2
    modbus.begin(MODBUS_ID, Serial2);

    modbus.preTransmission(preTransmission);
    modbus.postTransmission(postTransmission);

    // 4. UART0 (Serial) queda libre para ver mensajes en el PC  
    Serial.println("Comunicacion con Fronius iniciada en UART2...");
}

// =============================================================
void loop() {
    delay(100);
    static uint32_t lastPoll = 0;
    if (millis() - lastPoll < POLL_INTERVAL_MS) return;
    lastPoll = millis();

    runReadCycle();
    //updateOLED();
    printDebug();
}

void preTransmission() {
    // Flush cualquier byte residual antes de transmitir
    while (Serial2.available()) Serial2.read();
}

void postTransmission() {
    // Dar tiempo al módulo para volver a RX y vaciar el eco
    delay(2);
    while (Serial2.available()) Serial2.read();  // descartar eco propio
}

// =============================================================
//  Ciclo completo: SF -> MPPT1 -> MPPT2 -> Bloque AC
// =============================================================
void runReadCycle() {
    inv.valid = false;

    // Scale factors (si fallan, continua con valores por defecto)
    if (!readMPPTScaleFactors()) {
        Serial.println("[WARN] SF no leidos, usando sf=-2 (x0.01)");
    }

    // String 1 — obligatorio
    if (!readMPPTString(1, inv.Idc1, inv.Vdc1, inv.Pdc1)) {
        Serial.println("[ERR] Fallo MPPT1 — abortando ciclo");
        return;
    }

    // String 2 — no fatal (puede haber un solo string activo)
    if (!readMPPTString(2, inv.Idc2, inv.Vdc2, inv.Pdc2)) {
        Serial.println("[WARN] Fallo MPPT2 — asumiendo 0");
        inv.Idc2 = inv.Vdc2 = inv.Pdc2 = 0.0f;
    }

    inv.PdcTotal = inv.Pdc1 + inv.Pdc2;

    // Bloque AC — obligatorio
    if (!readACBlock()) {
        Serial.println("[ERR] Fallo bloque AC — abortando ciclo");
        return;
    }

    inv.valid = true;
}

// =============================================================
//  Scale factors del modelo Multiple MPPT
//  FC03, 3 registros desde 40265 (base-0)
// =============================================================
bool readMPPTScaleFactors() {
    for (int r = 0; r < MAX_RETRIES; r++) {
        uint8_t res = modbus.readHoldingRegisters(REG_MPPT_SF_BASE, 3);
        if (res == ModbusMaster::ku8MBSuccess) {
            sf_DCA = (int16_t)modbus.getResponseBuffer(0);
            sf_DCV = (int16_t)modbus.getResponseBuffer(1);
            sf_DCW = (int16_t)modbus.getResponseBuffer(2);
            Serial.printf("[SF] DCA=%d DCV=%d DCW=%d\n", sf_DCA, sf_DCV, sf_DCW);
            return true;
        }
        Serial.printf("[SF] retry %d err=0x%02X\n", r + 1, res);
        delay(150);
    }
    return false;
}

// =============================================================
//  Lectura de un string MPPT (I, V, P)
//  str: 1 o 2   |   FC03, 3 registros contiguos
// =============================================================
bool readMPPTString(uint8_t str, float &I, float &V, float &P) {
    uint16_t base = (str == 1) ? REG_MPPT1_BASE : REG_MPPT2_BASE;

    for (int r = 0; r < MAX_RETRIES; r++) {
        uint8_t res = modbus.readHoldingRegisters(base, 3);
        if (res == ModbusMaster::ku8MBSuccess) {
            uint16_t rawI = modbus.getResponseBuffer(0);
            uint16_t rawV = modbus.getResponseBuffer(1);
            uint16_t rawW = modbus.getResponseBuffer(2);

            // 0xFFFF = dato invalido (arranque/parada)
            // Documentado en TFG seccion 5.6 y mapa de registros
            I = (rawI == 0xFFFF) ? 0.0f : applyScaleFactor(rawI, sf_DCA);
            V = (rawV == 0xFFFF) ? 0.0f : applyScaleFactor(rawV, sf_DCV);
            P = (rawW == 0xFFFF) ? 0.0f : applyScaleFactor(rawW, sf_DCW);

            Serial.printf("[MPPT%d] I=%.3fA V=%.2fV P=%.1fW\n", str, I, V, P);
            return true;
        }
        Serial.printf("[MPPT%d] retry %d err=0x%02X\n", str, r + 1, res);
        delay(150);
    }
    return false;
}

// =============================================================
//  Bloque AC: 47 registros desde 40071 (base-0)
//  Decodifica float32 big-endian y el estado uint16
// =============================================================
bool readACBlock() {
    for (int r = 0; r < MAX_RETRIES; r++) {
        uint8_t res = modbus.readHoldingRegisters(REG_AC_BASE, REG_AC_COUNT);
        if (res == ModbusMaster::ku8MBSuccess) {
            // float32: word alto en offset N, word bajo en N+1
            inv.Iac   = regsToFloat(
                modbus.getResponseBuffer(OFF_AC_CURRENT),
                modbus.getResponseBuffer(OFF_AC_CURRENT + 1));
            inv.Vac   = regsToFloat(
                modbus.getResponseBuffer(OFF_AC_VOLTAGE),
                modbus.getResponseBuffer(OFF_AC_VOLTAGE + 1));
            inv.Pac   = regsToFloat(
                modbus.getResponseBuffer(OFF_AC_POWER),
                modbus.getResponseBuffer(OFF_AC_POWER + 1));
            inv.state = modbus.getResponseBuffer(OFF_STATE);

            Serial.printf("[AC] I=%.3fA V=%.2fV P=%.1fW St=%s\n",
                 inv.Iac, inv.Vac, inv.Pac, stateStr(inv.state));
            return true;
        }
        Serial.printf("[AC] retry %d err=0x%02X\n", r + 1, res);
        delay(150);
    }
    return false;
}

// =============================================================
//  Scale factor SunSpec: valor_fisico = raw * 10^sf
// =============================================================
float applyScaleFactor(uint16_t raw, int16_t sf) {
    float v = (float)raw;
    if (sf >= 0) {
        for (int i = 0; i < sf;  i++) v *= 10.0f;
    } else {
        for (int i = 0; i < -sf; i++) v *= 0.1f;
    }
    return v;
}

// =============================================================
//  Reconstruir float32 IEEE-754 big-endian desde dos uint16
// =============================================================
float regsToFloat(uint16_t hi, uint16_t lo) {
    uint32_t raw = ((uint32_t)hi << 16) | (uint32_t)lo;
    float f;
    memcpy(&f, &raw, sizeof(f));
    return f;
}

// =============================================================
//  Nombre del estado SunSpec enum16
// =============================================================
const char* stateStr(uint16_t st) {
    switch (st) {
        case 1: return "OFF";
        case 2: return "SLEEPING";
        case 3: return "STARTING";
        case 4: return "MPPT";
        case 5: return "THROTTLE";
        case 6: return "SHUTTING";
        case 7: return "FAULT";
        case 8: return "STANDBY";
        default: return "UNKNOWN";
    }
}

// =============================================================
//  Resultados por Serial1 (debug)
// =============================================================
void printDebug() {
    if (!inv.valid) {
        Serial.println("[!] Error Modbus: El inversor no responde.");
        return;
    }

    Serial.println("\n+---------------------------------------+");
    Serial.printf("| FRONIUS ID: %-3d | ESTADO: %-10s |\n", MODBUS_ID, stateStr(inv.state));
    Serial.println("+---------------------------------------+");
    Serial.printf("| AC Power: %7.1f W | VAC: %5.1f V |\n", inv.Pac, inv.Vac);
    Serial.printf("| DC Total: %7.1f W | IAC: %5.2f A |\n", inv.PdcTotal, inv.Iac);
    Serial.println("+---------------------------------------+");
}

I tried different MODBUS_ID like 1, 100 or 101 and nothing. In case you need smthn more, just ask for it! ;)

have you an oscilloscope to look at the A and B signals?

Yes, I tried to see the signals with the oscilloscope, and nothing!

are the ESP32 TX and RX signals showing anything?

try swopping the ESP32 Tx and Rx signals

e.g. I use

// RS485 VCC ESP32 to to 3.3V
#define RXD2 16  //  ESP32 Serial1 Rx Receive  pin 16 to RS485 RO  (Receiver Output) /RXD
#define TXD2 17  //  ESP32 Serial1 Tx Transmit pin 17 to RS485 DI  (Driver Input) /TXD

I have already tried that configuration and no answers from the inverter. Right now, I'm doing the comms with the ESP32 PLC 21+. I'll keep you posted if I manage to communicate with the inverter using this PLC

if you check your transmitted signals with an oscilloscope should look something like

shows original TTL signal from ESP32 and the resultant RS485 A and B differential signals

Sorry for the delay. I decided to start with the other modules of the project first, and I have finally managed to communicate with the inverter. I decided to replace the Fronius inverter with the Huawei SUN2000-2KTL-L1, as it appeared that the Datamanager was blocked for some reason. I also changed the MAX3485 module to the 5 V MAX485 and performed the DE/RE operations manually. The other one didn't respect the times, but now everything works as it should.

Here you have a picture with the connections: