#include #include #include #include #include #include #include "config.h" #include "config_manager.h" extern "C" void SystemClock_Config(void) { RCC_OscInitTypeDef osc = {}; RCC_ClkInitTypeDef clk = {}; RCC_PeriphCLKInitTypeDef periphClk = {}; osc.OscillatorType = RCC_OSCILLATORTYPE_HSE; osc.HSEState = RCC_HSE_ON; osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; osc.PLL.PLLMUL = RCC_PLL_MUL9; if (HAL_RCC_OscConfig(&osc) == HAL_OK) { clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; clk.APB1CLKDivider = RCC_HCLK_DIV2; clk.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2); periphClk.PeriphClockSelection = RCC_PERIPHCLK_USB; periphClk.UsbClockSelection = RCC_USBCLKSOURCE_PLL_DIV1_5; HAL_RCCEx_PeriphCLKConfig(&periphClk); } else { osc.OscillatorType = RCC_OSCILLATORTYPE_HSI; osc.HSEState = RCC_HSE_OFF; osc.HSIState = RCC_HSI_ON; osc.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; osc.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2; osc.PLL.PLLMUL = RCC_PLL_MUL12; HAL_RCC_OscConfig(&osc); clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; clk.APB1CLKDivider = RCC_HCLK_DIV2; clk.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_1); periphClk.PeriphClockSelection = RCC_PERIPHCLK_USB; periphClk.UsbClockSelection = RCC_USBCLKSOURCE_PLL; HAL_RCCEx_PeriphCLKConfig(&periphClk); } } // Hält PA8 HIGH (USB getrennt) bis setup() es manuell verbindet extern "C" void USBD_reenumerate(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio = {}; gpio.Pin = GPIO_PIN_8; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); } HardwareSerial DebugSerial(PA10, PA9); // USB-Diagnose: was der Wechselrichter per Control-Transfer schickt extern __IO bool dtrState; extern __IO bool rtsState; struct { uint32_t baud; uint8_t format; uint8_t parity; uint8_t bits; } g_lineCoding = {115200, 0, 0, 8}; volatile bool g_lineCodingUpdated = false; // Wird vom USB-Stack aufgerufen wenn Wechselrichter SET_LINE_CODING schickt // usbd_cdc_if.c kann nicht geändert werden → wir lesen linecoding-Struct direkt extern "C" { typedef struct { uint32_t bitrate; uint8_t format; uint8_t paritytype; uint8_t datatype; } USBD_CDC_LineCodingTypeDef; extern USBD_CDC_LineCodingTypeDef linecoding; } // CDC SERIAL_STATE Notification — NuttX sendet dies beim Öffnen von /dev/ttyACM0 // signalisiert dem Wechselrichter DCD+DSR=1 → startet Modbus-Server im Inverter #include "usbd_core.h" extern USBD_HandleTypeDef hUSBD_Device_CDC; #define CDC_SERIAL_STATE_EP 0x82U void sendSerialStateNotification() { static uint8_t notif[10] = { 0xA1, 0x20, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00 }; USBD_LL_Transmit(&hUSBD_Device_CDC, CDC_SERIAL_STATE_EP, notif, sizeof(notif)); } // ModbusMaster schreibt Byte-für-Byte. Würden wir sofort senden, käme Byte 1 als // eigenes 1-Byte USB-Paket an — der Wechselrichter behandelt jedes Short Packet // als Frame-Ende und sieht keinen gültigen Modbus-Frame. // Lösung: Bytes puffern, dann in flush() als ein einziges USB-Paket senden. class ModbusStream : public Stream { public: uint8_t txBuf[16]; uint8_t txLen = 0; uint8_t rxBuf[32]; uint8_t rxLen = 0; uint32_t rxTotal = 0; void reset() { txLen = 0; rxLen = 0; } int available() override { return SerialUSB.available(); } int read() override { int b = SerialUSB.read(); if (b >= 0) { rxTotal++; if (rxLen < sizeof(rxBuf)) rxBuf[rxLen++] = (uint8_t)b; } return b; } int peek() override { return SerialUSB.peek(); } size_t write(uint8_t b) override { if (txLen < sizeof(txBuf)) txBuf[txLen++] = b; return 1; } size_t write(const uint8_t* buf, size_t size) override { for (size_t i = 0; i < size && txLen < sizeof(txBuf); i++) txBuf[txLen++] = buf[i]; return size; } void flush() override { if (txLen > 0) SerialUSB.write(txBuf, txLen); // txLen nicht zurücksetzen — Debug-Code liest txBuf danach noch } using Print::write; } modbusSerial; // ============================================================ // Sensor-Definitionen // ============================================================ struct Sensor { const char* id; const char* name; uint16_t address; bool isDword; float scale; const char* unit; const char* deviceClass; const char* stateClass; const char* icon; }; #if defined(INVERTER_MIC1500) const Sensor SENSORS[] = { {"pv_voltage", "PV Voltage", 3, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"}, {"pv_current", "PV Current", 4, false, 0.1f, "A", "current", "measurement", "mdi:solar-panel"}, {"pv_power", "PV Power", 5, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"}, {"grid_frequency", "Grid Frequency", 37, false, 0.01f, "Hz", "frequency", "measurement", "mdi:sine-wave"}, {"grid_voltage", "Grid Voltage", 38, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"}, {"grid_current", "Grid Current", 39, false, 0.1f, "A", "current", "measurement", "mdi:flash"}, {"ac_power", "AC Output Power", 40, true, 0.1f, "W", "power", "measurement", "mdi:flash"}, {"energy_today", "Energy Today", 53, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"}, {"energy_total", "Energy Total", 55, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"}, {"inverter_temp", "Inverter Temperature", 93, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"}, }; #elif defined(INVERTER_SPH5000) const Sensor SENSORS[] = { {"pv1_voltage", "PV1 Voltage", 3, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"}, {"pv1_current", "PV1 Current", 4, false, 0.1f, "A", "current", "measurement", "mdi:solar-panel"}, {"pv1_power", "PV1 Power", 5, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"}, {"pv2_voltage", "PV2 Voltage", 7, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"}, {"pv2_current", "PV2 Current", 8, false, 0.1f, "A", "current", "measurement", "mdi:solar-panel"}, {"pv2_power", "PV2 Power", 9, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"}, {"ac_power_total", "AC Output Power Total", 35, true, 0.1f, "W", "power", "measurement", "mdi:flash"}, {"grid_frequency", "Grid Frequency", 37, false, 0.01f, "Hz", "frequency", "measurement", "mdi:sine-wave"}, {"grid_voltage_l1", "Grid Voltage L1", 38, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"}, {"grid_current_l1", "Grid Current L1", 39, false, 0.1f, "A", "current", "measurement", "mdi:flash"}, {"grid_voltage_l2", "Grid Voltage L2", 42, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"}, {"grid_current_l2", "Grid Current L2", 43, false, 0.1f, "A", "current", "measurement", "mdi:flash"}, {"grid_voltage_l3", "Grid Voltage L3", 46, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"}, {"grid_current_l3", "Grid Current L3", 47, false, 0.1f, "A", "current", "measurement", "mdi:flash"}, {"energy_today", "Energy Today", 53, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"}, {"energy_total", "Energy Total", 55, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"}, {"inverter_temp", "Inverter Temperature", 93, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"}, {"bat_discharge_power", "Battery Discharge Power", 1009, true, 0.1f, "W", "power", "measurement", "mdi:battery-minus"}, {"bat_charge_power", "Battery Charge Power", 1011, true, 0.1f, "W", "power", "measurement", "mdi:battery-plus"}, {"bat_voltage", "Battery Voltage", 1013, false, 0.1f, "V", "voltage", "measurement", "mdi:battery"}, {"bat_soc", "Battery State of Charge", 1014, false, 1.0f, "%", "battery", "measurement", "mdi:battery"}, {"bat_temperature", "Battery Temperature", 1040, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"}, {"power_to_grid", "Power To Grid", 1021, true, 0.1f, "W", "power", "measurement", "mdi:transmission-tower-export"}, {"power_to_user", "Power To User", 1029, true, 0.1f, "W", "power", "measurement", "mdi:transmission-tower-import"}, {"energy_import_total", "Energy Import Total", 1046, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:transmission-tower-import"}, {"energy_export_total", "Energy Export Total", 1050, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:transmission-tower-export"}, {"bat_discharge_total", "Battery Discharge Total", 1054, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:battery-minus"}, {"bat_charge_total", "Battery Charge Total", 1058, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:battery-plus"}, }; #else #error "Kein Inverter-Modell definiert! INVERTER_MIC1500 oder INVERTER_SPH5000 in config.h setzen." #endif const uint8_t SENSOR_COUNT = sizeof(SENSORS) / sizeof(SENSORS[0]); // ============================================================ // Globale Objekte // ============================================================ NetConfig cfg; byte mac[] = {MAC_ADDRESS}; EthernetClient ethClient; PubSubClient mqtt(ethClient); ModbusMaster modbus; EthernetServer webServer(80); uint32_t g_okTotal = 0; uint32_t g_failTotal = 0; uint8_t g_lastErr = 0; uint32_t g_rawTotal = 0; // ============================================================ // Web-Server // ============================================================ static bool webReadLine(EthernetClient& c, char* buf, size_t maxLen) { size_t i = 0; uint32_t t = millis(); while (millis() - t < 1000) { while (c.available()) { char ch = c.read(); if (ch == '\n') { buf[i] = 0; return true; } if (ch != '\r' && i < maxLen - 1) buf[i++] = ch; t = millis(); } } buf[i] = 0; return false; } static void ipToStr(const uint8_t ip[4], char* buf) { snprintf(buf, 16, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]); } static void urlDecode(char* s) { char* out = s; while (*s) { if (*s == '+') { *out++ = ' '; s++; } else if (*s == '%' && s[1] && s[2]) { char h[3] = {s[1], s[2], 0}; *out++ = (char)strtol(h, nullptr, 16); s += 3; } else { *out++ = *s++; } } *out = 0; } static void parseIP(const char* s, uint8_t out[4]) { unsigned a = 0, b = 0, c = 0, d = 0; if (sscanf(s, "%u.%u.%u.%u", &a, &b, &c, &d) == 4) { out[0] = a; out[1] = b; out[2] = c; out[3] = d; } } static void parseFormBody(char* body) { cfg.useDhcp = false; // Checkbox fehlt im POST wenn nicht gesetzt char* p = body; while (p && *p) { char* eq = strchr(p, '='); char* amp = strchr(p, '&'); if (!eq) break; *eq = 0; if (amp) *amp = 0; char* key = p; char* val = eq + 1; urlDecode(key); urlDecode(val); if (!strcmp(key, "dhcp")) cfg.useDhcp = true; else if (!strcmp(key, "ip")) parseIP(val, cfg.ip); else if (!strcmp(key, "gw")) parseIP(val, cfg.gw); else if (!strcmp(key, "sn")) parseIP(val, cfg.sn); else if (!strcmp(key, "dns")) parseIP(val, cfg.dns); else if (!strcmp(key, "broker")) strncpy(cfg.mqttBroker, val, sizeof(cfg.mqttBroker) - 1); else if (!strcmp(key, "port")) cfg.mqttPort = (uint16_t)atoi(val); else if (!strcmp(key, "user")) strncpy(cfg.mqttUser, val, sizeof(cfg.mqttUser) - 1); else if (!strcmp(key, "pass")) strncpy(cfg.mqttPass, val, sizeof(cfg.mqttPass) - 1); else if (!strcmp(key, "interval")) cfg.updateMs = (uint32_t)atoi(val) * 1000UL; p = amp ? amp + 1 : nullptr; } } static void sendPage(EthernetClient& cl) { char buf[16]; cl.print( "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n" "" "" "" "ShineLAN-X" ); // Header cl.print("

☀ ShineLAN-X

" "
Growatt Modbus → MQTT Bridge
" "
Verbunden" : "off'>Getrennt"); cl.print("
IP: "); cl.print(Ethernet.localIP()); cl.print("
"); // Statistik cl.print("
" "
"); cl.print(g_okTotal); cl.print("
OK
" "
"); cl.print(g_failTotal); cl.print("
Fehler
" "
0x"); snprintf(buf, sizeof(buf), "%02X", g_lastErr); cl.print(buf); cl.print("
Letzter Err
"); // Formular — Netzwerk cl.print("
" "

Netzwerk

" "
" "
"); ipToStr(cfg.ip, buf); cl.print("
" "
"); ipToStr(cfg.gw, buf); cl.print("
" "
"); ipToStr(cfg.sn, buf); cl.print("
" "
"); ipToStr(cfg.dns, buf); cl.print("
" "
"); cl.print("
"); // Formular — MQTT cl.print("

MQTT

" "
" "
" "
" "
" "
" "
" "
" "
" "
" "
"); // Buttons + Script cl.print( "" "" "
" "" ); } static void handleWebRequest() { EthernetClient client = webServer.available(); if (!client) return; char line[80]; if (!webReadLine(client, line, sizeof(line))) { client.stop(); return; } bool isPost = (strncmp(line, "POST", 4) == 0); bool isReset = (strstr(line, "/reset") != nullptr); // Header lesen, Content-Length suchen (case-insensitive via tolower) int contentLen = 0; char hdr[80]; while (webReadLine(client, hdr, sizeof(hdr)) && hdr[0] != '\0') { for (char* p = hdr; *p; p++) *p = (char)tolower((unsigned char)*p); if (strncmp(hdr, "content-length:", 15) == 0) contentLen = atoi(hdr + 15); } if (isReset) { configDefaults(cfg); configSave(cfg); client.print("HTTP/1.1 303 See Other\r\nLocation: /\r\nConnection: close\r\n\r\n"); client.flush(); client.stop(); delay(300); NVIC_SystemReset(); return; } if (isPost && contentLen > 0 && contentLen < 500) { char body[500] = {}; int n = 0; uint32_t t = millis(); while (n < contentLen && millis() - t < 3000) { if (client.available()) { body[n++] = client.read(); t = millis(); } } body[n] = 0; parseFormBody(body); cfg.magic = CFG_MAGIC; configSave(cfg); client.print("HTTP/1.1 303 See Other\r\nLocation: /\r\nConnection: close\r\n\r\n"); client.flush(); client.stop(); delay(300); NVIC_SystemReset(); return; } sendPage(client); client.flush(); client.stop(); } // ============================================================ // LED // ============================================================ void ledSet(uint8_t pin, bool on) { digitalWrite(pin, on ? LOW : HIGH); } // ============================================================ // MQTT // ============================================================ void publishDiscovery() { char topic[128]; char payload[640]; for (uint8_t i = 0; i < SENSOR_COUNT; i++) { const Sensor& s = SENSORS[i]; snprintf(topic, sizeof(topic), "homeassistant/sensor/%s_%s/config", DEVICE_ID, s.id); snprintf(payload, sizeof(payload), "{" "\"name\":\"%s\"," "\"unique_id\":\"%s_%s\"," "\"state_topic\":\"growatt/shinelan/%s\"," "\"unit_of_measurement\":\"%s\"," "\"device_class\":\"%s\"," "\"state_class\":\"%s\"," "\"icon\":\"%s\"," "\"device\":{" "\"identifiers\":[\"%s\"]," "\"name\":\"%s\"," "\"model\":\"%s\"," "\"manufacturer\":\"%s\"" "}}", s.name, DEVICE_ID, s.id, s.id, s.unit, s.deviceClass, s.stateClass, s.icon, DEVICE_ID, DEVICE_NAME, DEVICE_MODEL, DEVICE_MFR); mqtt.publish(topic, payload, true); } } bool mqttReconnect() { DebugSerial.print("MQTT connecting... "); ledSet(LED_RED, true); bool ok = (strlen(cfg.mqttUser) > 0) ? mqtt.connect(MQTT_CLIENT, cfg.mqttUser, cfg.mqttPass) : mqtt.connect(MQTT_CLIENT); if (ok) { DebugSerial.println("OK"); ledSet(LED_RED, false); ledSet(LED_GREEN, true); publishDiscovery(); } else { DebugSerial.print("FAIL rc="); DebugSerial.println(mqtt.state()); } return ok; } // ============================================================ // Setup // ============================================================ void setup() { DebugSerial.begin(115200); delay(10); DebugSerial.println("\r\n=== Growatt ShineLAN-X ==="); DebugSerial.println("Build: " __DATE__ " " __TIME__); configLoad(cfg); pinMode(LED_DEBUG, OUTPUT); ledSet(LED_DEBUG, false); pinMode(LED_RED, OUTPUT); ledSet(LED_RED, false); pinMode(LED_GREEN, OUTPUT); ledSet(LED_GREEN, false); pinMode(LED_BLUE, OUTPUT); ledSet(LED_BLUE, false); ledSet(LED_RED, true); ledSet(LED_GREEN, true); ledSet(LED_BLUE, true); delay(300); ledSet(LED_RED, false); ledSet(LED_GREEN, false); ledSet(LED_BLUE, false); pinMode(ETH_RST_PIN, OUTPUT); digitalWrite(ETH_RST_PIN, LOW); delay(20); digitalWrite(ETH_RST_PIN, HIGH); delay(200); Ethernet.init(ETH_CS_PIN); if (cfg.useDhcp) { DebugSerial.println("ETH: DHCP..."); if (Ethernet.begin(mac) == 0) { // DHCP fehlgeschlagen: Fallback auf gespeicherte statische IP DebugSerial.println("ETH: DHCP failed, static fallback"); IPAddress ip (cfg.ip[0], cfg.ip[1], cfg.ip[2], cfg.ip[3]); IPAddress gw (cfg.gw[0], cfg.gw[1], cfg.gw[2], cfg.gw[3]); IPAddress sn (cfg.sn[0], cfg.sn[1], cfg.sn[2], cfg.sn[3]); IPAddress dns(cfg.dns[0], cfg.dns[1], cfg.dns[2], cfg.dns[3]); Ethernet.begin(mac, ip, dns, gw, sn); } } else { IPAddress ip (cfg.ip[0], cfg.ip[1], cfg.ip[2], cfg.ip[3]); IPAddress gw (cfg.gw[0], cfg.gw[1], cfg.gw[2], cfg.gw[3]); IPAddress sn (cfg.sn[0], cfg.sn[1], cfg.sn[2], cfg.sn[3]); IPAddress dns(cfg.dns[0], cfg.dns[1], cfg.dns[2], cfg.dns[3]); Ethernet.begin(mac, ip, dns, gw, sn); } DebugSerial.print("ETH: IP="); DebugSerial.println(Ethernet.localIP()); if (Ethernet.linkStatus() == LinkON) ledSet(LED_DEBUG, true); webServer.begin(); DebugSerial.print("Web: http://"); DebugSerial.println(Ethernet.localIP()); mqtt.setServer(cfg.mqttBroker, cfg.mqttPort); mqtt.setBufferSize(768); SerialUSB.begin(MODBUS_BAUD); delay(10000); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET); // Warte bis Wechselrichter USB-CDC konfiguriert hat delay(3000); // SERIAL_STATE(DCD=1,DSR=1) senden — NuttX tut dies beim Öffnen von ttyACM0 // signalisiert dem Wechselrichter dass das Device bereit ist sendSerialStateNotification(); delay(500); modbus.begin(MODBUS_ADDR, modbusSerial); DebugSerial.println("Setup done."); } // ============================================================ // Loop // ============================================================ unsigned long lastUpdate = 0; static bool g_usbInfoPublished = false; void loop() { handleWebRequest(); if (!mqtt.connected()) { if (!mqttReconnect()) { delay(5000); return; } } mqtt.loop(); handleWebRequest(); // usbinfo: initial publish + DTR-Änderungen { static bool lastDtr = false; if (!g_usbInfoPublished || (bool)dtrState != lastDtr) { g_usbInfoPublished = true; lastDtr = (bool)dtrState; char info[80]; snprintf(info, sizeof(info), "baud=%lu fmt=%u par=%u bits=%u dtr=%d rts=%d", (unsigned long)linecoding.bitrate, linecoding.format, linecoding.paritytype, linecoding.datatype, (int)dtrState, (int)rtsState); mqtt.publish("growatt/shinelan/usbinfo", info, true); } } // SERIAL_STATE(DCD=1,DSR=1) alle 5s senden bis DTR vom Wechselrichter gesetzt wird { static uint32_t lastNotif = 0; if (!dtrState && millis() - lastNotif > 5000) { lastNotif = millis(); sendSerialStateNotification(); } } // Raw-RX-Drain: jedes Byte das außerhalb von Modbus-Fenstern ankommt fangen // Beantwortet: sendet der Inverter JEMALS irgendwas (spontan oder verspätet)? { static uint8_t rawBuf[32] = {}; static uint8_t rawLen = 0; static uint32_t rawPublish = 0; while (SerialUSB.available()) { uint8_t b = (uint8_t)SerialUSB.read(); g_rawTotal++; if (rawLen < sizeof(rawBuf)) rawBuf[rawLen++] = b; } if (rawLen > 0 && millis() - rawPublish > 1000) { rawPublish = millis(); char msg[80]; size_t p = snprintf(msg, sizeof(msg), "raw=%lu: ", g_rawTotal); for (uint8_t i = 0; i < rawLen && p < 74; i++) p += snprintf(msg + p, sizeof(msg) - p, "%02X", rawBuf[i]); mqtt.publish("growatt/shinelan/rawbytes", msg); rawLen = 0; } } if (millis() - lastUpdate < cfg.updateMs) return; lastUpdate = millis(); ledSet(LED_BLUE, true); uint32_t okCnt = 0, failCnt = 0; uint8_t lastErr = 0; uint16_t lastErrReg = 0; for (uint8_t i = 0; i < SENSOR_COUNT; i++) { const Sensor& s = SENSORS[i]; modbusSerial.reset(); uint8_t res = modbus.readInputRegisters(s.address, s.isDword ? 2 : 1); char dbg[80]; size_t pos = snprintf(dbg, sizeof(dbg), "ok=%lu fail=%lu err=0x%02X@%u avS=%d dtr=%d raw=%lu rx=%u tx=", g_okTotal + okCnt, g_failTotal + failCnt, lastErr, lastErrReg, SerialUSB.availableForWrite(), (int)dtrState, g_rawTotal, modbusSerial.rxLen); for (uint8_t j = 0; j < modbusSerial.txLen && pos < 74; j++) pos += snprintf(dbg + pos, sizeof(dbg) - pos, "%02X", modbusSerial.txBuf[j]); pos += snprintf(dbg + pos, sizeof(dbg) - pos, " rx="); for (uint8_t j = 0; j < modbusSerial.rxLen && pos < 78; j++) pos += snprintf(dbg + pos, sizeof(dbg) - pos, "%02X", modbusSerial.rxBuf[j]); mqtt.publish("growatt/shinelan/debug", dbg); delay(10); // mbusd -R10: 10ms Pause zwischen Requests if (res != modbus.ku8MBSuccess) { failCnt++; lastErr = res; lastErrReg = s.address; continue; } okCnt++; float val; if (s.isDword) { uint32_t raw = ((uint32_t)modbus.getResponseBuffer(0) << 16) | modbus.getResponseBuffer(1); val = raw * s.scale; } else { val = modbus.getResponseBuffer(0) * s.scale; } char topic[64], payload[24]; snprintf(topic, sizeof(topic), "growatt/shinelan/%s", s.id); snprintf(payload, sizeof(payload), "%.1f", (double)val); mqtt.publish(topic, payload); } g_okTotal += okCnt; g_failTotal += failCnt; g_lastErr = lastErr; ledSet(LED_BLUE, false); }