ShineLAN-X: Web-Konfigurations-UI + EEPROM-Config
- Tasmota-artiges Web-Interface auf Port 80 (DHCP/IP, MQTT, Intervall) - NetConfig-Struct in EEPROM (Magic 0xA55A1234, Defaults bei erstem Boot) - config.h bereinigt: keine Geheimnisse mehr, nur DEFAULT_*-Werte - config_manager.h neu: configLoad/Save/Defaults - USB CDC Build-Flags wiederhergestellt (-DUSBCON etc.) - SPH 5000: power_to_grid (1021) + power_to_user (1029) ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
# Growatt ShineLAN-X Firmware
|
||||||
|
|
||||||
|
PlatformIO-Projekt fuer STM32F103RBT6. Liest Modbus RTU vom Wechselrichter via USB-CDC und publiziert via MQTT an Home Assistant.
|
||||||
|
|
||||||
|
## Wiki Knowledge Base
|
||||||
|
|
||||||
|
Path: /Users/retr0/Nextcloud/HomeLab/Vault
|
||||||
|
|
||||||
|
Beim Start einer neuen Session:
|
||||||
|
1. Zuerst `wiki/hot.md` lesen (aktueller Kontext)
|
||||||
|
2. Dann `wiki/entities/Growatt ShineLAN-X Firmware.md` fuer Projektdetails
|
||||||
|
3. Nur bei Bedarf weitere Wiki-Seiten
|
||||||
|
|
||||||
|
## Wichtige Konventionen
|
||||||
|
|
||||||
|
- Kommentare nur wenn das WARUM nicht offensichtlich ist
|
||||||
|
- Keine trailing summaries nach Tool-Calls
|
||||||
|
- config.h ist geheimnis-frei (DEFAULT_* Werte) -- committen OK
|
||||||
|
- Echte Credentials werden nur im EEPROM gespeichert, nie im Source
|
||||||
|
- Framework-Hacks in `~/.platformio/` werden bei `pio pkg update` zurueckgesetzt
|
||||||
@@ -1,71 +1,62 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Pin-Belegung — Quelle: https://github.com/mwalle/shinelanx-modbus
|
// Pin-Belegung — STM32F103RBT6, LQFP-64
|
||||||
// Gleiche Platine, verifizierte Pins
|
// Quelle: https://github.com/mwalle/shinelanx-modbus
|
||||||
// STM32F103RBT6, LQFP-64
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// ENC28J60 — SPI2 (Hardware-SPI)
|
// ENC28J60 — SPI2 (Hardware-SPI)
|
||||||
// LQFP-64: PB12=33, PB13=34, PB14=35, PB15=36, PC6=37, PC8=39
|
#define ETH_CS_PIN PB12
|
||||||
#define ETH_CS_PIN PB12 // ENC28J60 /CS (SPI2 NSS)
|
#define ETH_SCK_PIN PB13
|
||||||
#define ETH_SCK_PIN PB13 // ENC28J60 SCK (SPI2 SCK)
|
#define ETH_MISO_PIN PB14
|
||||||
#define ETH_MISO_PIN PB14 // ENC28J60 SO (SPI2 MISO)
|
#define ETH_MOSI_PIN PB15
|
||||||
#define ETH_MOSI_PIN PB15 // ENC28J60 SI (SPI2 MOSI)
|
#define ETH_RST_PIN PC8
|
||||||
#define ETH_RST_PIN PC8 // ENC28J60 /RESET
|
|
||||||
#define ETH_INT_PIN PC6 // ENC28J60 INT# (optional, polling reicht)
|
|
||||||
|
|
||||||
// LEDs
|
// LEDs (aktiv LOW)
|
||||||
#define LED_DEBUG PC7 // Debug-LED (grün o.ä.)
|
#define LED_DEBUG PC7
|
||||||
#define LED_RED PB1 // RGB Rot
|
#define LED_RED PB1
|
||||||
#define LED_GREEN PB0 // RGB Grün
|
#define LED_GREEN PB0
|
||||||
#define LED_BLUE PC5 // RGB Blau
|
#define LED_BLUE PC5
|
||||||
|
|
||||||
// Taster
|
// User-Taster
|
||||||
#define BTN_USER PA3 // User-Taster (low-aktiv)
|
#define BTN_USER PA3
|
||||||
|
|
||||||
// ============================================================
|
// MAC-Adresse
|
||||||
// NETZWERK
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// 0 = DHCP, 1 = Statische IP
|
|
||||||
#define USE_DHCP 1
|
|
||||||
|
|
||||||
// Nur relevant wenn USE_DHCP = 0
|
|
||||||
#define STATIC_IP 192,168,2,15
|
|
||||||
#define STATIC_GW 192,168,2,1
|
|
||||||
#define STATIC_SUBNET 255,255,255,0
|
|
||||||
#define STATIC_DNS 192,168,2,1
|
|
||||||
|
|
||||||
// MAC-Adresse — muss im Netzwerk eindeutig sein
|
|
||||||
#define MAC_ADDRESS 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
|
#define MAC_ADDRESS 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// MQTT
|
// MODBUS
|
||||||
// ============================================================
|
// ============================================================
|
||||||
#define MQTT_BROKER "192.168.1.1"
|
#define MODBUS_BAUD 115200
|
||||||
#define MQTT_PORT 1883
|
#define MODBUS_ADDR 1
|
||||||
#define MQTT_USER "mqtt"
|
|
||||||
#define MQTT_PASSWORD "HIER_MQTT_PASSWORT_EINTRAGEN"
|
|
||||||
#define MQTT_CLIENT "growatt-shinelan"
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// MODBUS / WECHSELRICHTER-KOMMUNIKATION
|
|
||||||
// Growatt kommuniziert über USB-CDC (virtueller COM-Port) bei 115200 Baud.
|
|
||||||
// STM32 agiert als USB-Device (CDC), Wechselrichter ist USB-Host.
|
|
||||||
// Physikalisch: PA11=D-, PA12=D+, PA8=USB-Pullup-Steuerpin
|
|
||||||
// Kein RS485, kein DE/RE-Pin nötig.
|
|
||||||
// ============================================================
|
|
||||||
#define MODBUS_BAUD 115200 // Growatt USB-CDC Baudrate
|
|
||||||
#define MODBUS_ADDR 1 // Modbus Slave-Adresse des Wechselrichters
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// GERÄT
|
// GERÄT
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
#define MQTT_CLIENT "growatt-shinelan"
|
||||||
#define DEVICE_ID "growatt_shinelan"
|
#define DEVICE_ID "growatt_shinelan"
|
||||||
#define DEVICE_NAME "Growatt ShineLAN-X"
|
#define DEVICE_NAME "Growatt ShineLAN-X"
|
||||||
#define DEVICE_MODEL "ShineLAN-X"
|
#define DEVICE_MODEL "ShineLAN-X"
|
||||||
#define DEVICE_MFR "Growatt"
|
#define DEVICE_MFR "Growatt"
|
||||||
|
|
||||||
// Abfrageintervall in Millisekunden
|
// ============================================================
|
||||||
#define UPDATE_INTERVAL 10000UL
|
// STANDARD-WERTE (erster Boot / Werkseinstellungen)
|
||||||
|
// Werden über die Web-UI überschrieben und in EEPROM gespeichert.
|
||||||
|
// ============================================================
|
||||||
|
#define DEFAULT_DHCP true
|
||||||
|
#define DEFAULT_STATIC_IP 192,168,1,99
|
||||||
|
#define DEFAULT_GW 192,168,1,1
|
||||||
|
#define DEFAULT_SUBNET 255,255,255,0
|
||||||
|
#define DEFAULT_DNS 192,168,1,1
|
||||||
|
#define DEFAULT_MQTT_BROKER "192.168.1.1"
|
||||||
|
#define DEFAULT_MQTT_PORT 1883
|
||||||
|
#define DEFAULT_MQTT_USER ""
|
||||||
|
#define DEFAULT_MQTT_PASS ""
|
||||||
|
#define DEFAULT_UPDATE_MS 10000UL
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// WECHSELRICHTER-MODELL
|
||||||
|
// Genau einen einkommentieren:
|
||||||
|
// ============================================================
|
||||||
|
#define INVERTER_MIC1500 // Growatt MIC 1500/2000 TL-X (einphasig)
|
||||||
|
// #define INVERTER_SPH5000 // Growatt SPH 5000 TL3 (dreiphasig, Hybrid)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <EEPROM.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#define CFG_MAGIC 0xA55A1234
|
||||||
|
|
||||||
|
struct NetConfig {
|
||||||
|
uint32_t magic;
|
||||||
|
bool useDhcp;
|
||||||
|
uint8_t ip[4];
|
||||||
|
uint8_t gw[4];
|
||||||
|
uint8_t sn[4];
|
||||||
|
uint8_t dns[4];
|
||||||
|
char mqttBroker[40];
|
||||||
|
uint16_t mqttPort;
|
||||||
|
char mqttUser[32];
|
||||||
|
char mqttPass[64];
|
||||||
|
uint32_t updateMs;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void configDefaults(NetConfig& c) {
|
||||||
|
c.magic = CFG_MAGIC;
|
||||||
|
c.useDhcp = DEFAULT_DHCP;
|
||||||
|
uint8_t dip[] = {DEFAULT_STATIC_IP};
|
||||||
|
uint8_t dgw[] = {DEFAULT_GW};
|
||||||
|
uint8_t dsn[] = {DEFAULT_SUBNET};
|
||||||
|
uint8_t ddn[] = {DEFAULT_DNS};
|
||||||
|
memcpy(c.ip, dip, 4);
|
||||||
|
memcpy(c.gw, dgw, 4);
|
||||||
|
memcpy(c.sn, dsn, 4);
|
||||||
|
memcpy(c.dns, ddn, 4);
|
||||||
|
strncpy(c.mqttBroker, DEFAULT_MQTT_BROKER, sizeof(c.mqttBroker) - 1);
|
||||||
|
c.mqttBroker[sizeof(c.mqttBroker) - 1] = 0;
|
||||||
|
c.mqttPort = DEFAULT_MQTT_PORT;
|
||||||
|
strncpy(c.mqttUser, DEFAULT_MQTT_USER, sizeof(c.mqttUser) - 1);
|
||||||
|
c.mqttUser[sizeof(c.mqttUser) - 1] = 0;
|
||||||
|
strncpy(c.mqttPass, DEFAULT_MQTT_PASS, sizeof(c.mqttPass) - 1);
|
||||||
|
c.mqttPass[sizeof(c.mqttPass) - 1] = 0;
|
||||||
|
c.updateMs = DEFAULT_UPDATE_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void configLoad(NetConfig& c) {
|
||||||
|
EEPROM.get(0, c);
|
||||||
|
if (c.magic != CFG_MAGIC) configDefaults(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void configSave(const NetConfig& c) {
|
||||||
|
EEPROM.put(0, c);
|
||||||
|
}
|
||||||
@@ -13,4 +13,8 @@ lib_deps =
|
|||||||
|
|
||||||
build_flags =
|
build_flags =
|
||||||
-DPIO_FRAMEWORK_ARDUINO_NOERRNO
|
-DPIO_FRAMEWORK_ARDUINO_NOERRNO
|
||||||
|
-DUSBCON
|
||||||
|
-DUSBD_USE_CDC
|
||||||
|
-DHAL_PCD_MODULE_ENABLED
|
||||||
|
-DUSB_CDC_TRANSMIT_TIMEOUT=3000
|
||||||
-I include
|
-I include
|
||||||
|
|||||||
@@ -3,15 +3,81 @@
|
|||||||
#include <EthernetENC.h>
|
#include <EthernetENC.h>
|
||||||
#include <PubSubClient.h>
|
#include <PubSubClient.h>
|
||||||
#include <ModbusMaster.h>
|
#include <ModbusMaster.h>
|
||||||
|
#include <EEPROM.h>
|
||||||
#include "config.h"
|
#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);
|
||||||
|
}
|
||||||
|
|
||||||
// Debug-UART: USART1 TX=PA9, RX=PA10
|
|
||||||
HardwareSerial DebugSerial(PA10, PA9);
|
HardwareSerial DebugSerial(PA10, PA9);
|
||||||
|
|
||||||
// Modbus über USB-CDC (SerialUSB = PA11/PA12, Wechselrichter ist USB-Host)
|
// USBSerial::flush() wartet endlos wenn der Wechselrichter den IN-Endpoint nicht pollt.
|
||||||
|
// Dieser Wrapper ersetzt flush() durch eine No-Op.
|
||||||
|
class ModbusStream : public Stream {
|
||||||
|
public:
|
||||||
|
int available() override { return SerialUSB.available(); }
|
||||||
|
int read() override { return SerialUSB.read(); }
|
||||||
|
int peek() override { return SerialUSB.peek(); }
|
||||||
|
size_t write(uint8_t b) override { return SerialUSB.write(b); }
|
||||||
|
size_t write(const uint8_t* buf, size_t size) override { return SerialUSB.write(buf, size); }
|
||||||
|
void flush() override {}
|
||||||
|
using Print::write;
|
||||||
|
} modbusSerial;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Sensor-Definition
|
// Sensor-Definitionen
|
||||||
// ============================================================
|
// ============================================================
|
||||||
struct Sensor {
|
struct Sensor {
|
||||||
const char* id;
|
const char* id;
|
||||||
@@ -25,17 +91,27 @@ struct Sensor {
|
|||||||
const char* icon;
|
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[] = {
|
const Sensor SENSORS[] = {
|
||||||
// --- PV Eingang ---
|
|
||||||
{"pv1_voltage", "PV1 Voltage", 3, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"},
|
{"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_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"},
|
{"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_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_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"},
|
{"pv2_power", "PV2 Power", 9, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"},
|
||||||
// --- AC Ausgang / Netz ---
|
|
||||||
// HINWEIS: MIC 1500 TL-X hat AC Output Power auf Register 40 (nicht 35)!
|
|
||||||
// SPH 5000 TL3: Register 35 — für MIC 1500 auf 40 ändern
|
|
||||||
{"ac_power_total", "AC Output Power Total", 35, true, 0.1f, "W", "power", "measurement", "mdi:flash"},
|
{"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_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_voltage_l1", "Grid Voltage L1", 38, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"},
|
||||||
@@ -44,37 +120,299 @@ const Sensor SENSORS[] = {
|
|||||||
{"grid_current_l2", "Grid Current L2", 43, false, 0.1f, "A", "current", "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_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"},
|
{"grid_current_l3", "Grid Current L3", 47, false, 0.1f, "A", "current", "measurement", "mdi:flash"},
|
||||||
// --- Energie PV ---
|
|
||||||
{"energy_today", "Energy Today", 53, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"},
|
{"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"},
|
{"energy_total", "Energy Total", 55, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"},
|
||||||
// --- Temperatur ---
|
|
||||||
{"inverter_temp", "Inverter Temperature", 93, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"},
|
{"inverter_temp", "Inverter Temperature", 93, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"},
|
||||||
// --- Batterie ---
|
|
||||||
{"bat_discharge_power", "Battery Discharge Power", 1009, true, 0.1f, "W", "power", "measurement", "mdi:battery-minus"},
|
{"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_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_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_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"},
|
{"bat_temperature", "Battery Temperature", 1040, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"},
|
||||||
// --- Energiezähler ---
|
{"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_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"},
|
{"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_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"},
|
{"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]);
|
const uint8_t SENSOR_COUNT = sizeof(SENSORS) / sizeof(SENSORS[0]);
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Globale Objekte
|
// Globale Objekte
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
NetConfig cfg;
|
||||||
byte mac[] = {MAC_ADDRESS};
|
byte mac[] = {MAC_ADDRESS};
|
||||||
EthernetClient ethClient;
|
EthernetClient ethClient;
|
||||||
PubSubClient mqtt(ethClient);
|
PubSubClient mqtt(ethClient);
|
||||||
ModbusMaster modbus;
|
ModbusMaster modbus;
|
||||||
|
EthernetServer webServer(80);
|
||||||
|
|
||||||
|
uint32_t g_okTotal = 0;
|
||||||
|
uint32_t g_failTotal = 0;
|
||||||
|
uint8_t g_lastErr = 0;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// LED-Hilfsfunktionen
|
// Web-Server
|
||||||
// ============================================================
|
// ============================================================
|
||||||
void ledSet(uint8_t pin, bool on) { digitalWrite(pin, on ? LOW : HIGH); } // aktiv LOW
|
|
||||||
|
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"
|
||||||
|
"<!DOCTYPE html><html lang='de'><head>"
|
||||||
|
"<meta charset='utf-8'>"
|
||||||
|
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||||
|
"<title>ShineLAN-X</title><style>"
|
||||||
|
"*{box-sizing:border-box}"
|
||||||
|
"body{font-family:Arial,sans-serif;background:#1a1a2e;color:#eee;margin:0}"
|
||||||
|
"header{background:#16213e;padding:12px 18px;display:flex;align-items:center;"
|
||||||
|
"border-bottom:2px solid #e94560}"
|
||||||
|
"header h1{margin:0;font-size:1.1em;color:#e94560;flex:1}"
|
||||||
|
".sub{font-size:.78em;color:#64748b;margin-top:2px}"
|
||||||
|
".badge{padding:3px 10px;border-radius:10px;font-size:.78em;font-weight:bold}"
|
||||||
|
".on{background:#14532d;color:#4ade80}"
|
||||||
|
".off{background:#450a0a;color:#f87171}"
|
||||||
|
"main{max-width:520px;margin:14px auto;padding:0 12px}"
|
||||||
|
".stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:12px}"
|
||||||
|
".sbox{background:#16213e;border-radius:6px;padding:9px;text-align:center}"
|
||||||
|
".sval{font-size:1.4em;font-weight:bold;color:#e94560}"
|
||||||
|
".slbl{font-size:.7em;color:#64748b;margin-top:2px}"
|
||||||
|
".card{background:#16213e;border-radius:7px;padding:14px;margin-bottom:12px}"
|
||||||
|
".card h2{margin:0 0 12px;font-size:.72em;text-transform:uppercase;"
|
||||||
|
"letter-spacing:.08em;color:#64748b;"
|
||||||
|
"border-bottom:1px solid #0f3460;padding-bottom:7px}"
|
||||||
|
".row{display:flex;align-items:center;gap:8px;margin-bottom:9px}"
|
||||||
|
".row label{min-width:115px;font-size:.84em;color:#94a3b8}"
|
||||||
|
".row input[type=text],.row input[type=number],.row input[type=password]"
|
||||||
|
"{flex:1;background:#0f3460;border:1px solid #1e4a80;color:#eee;"
|
||||||
|
"padding:6px 9px;border-radius:4px;font-size:.84em;min-width:0}"
|
||||||
|
".row input[type=checkbox]{width:16px;height:16px;accent-color:#e94560;cursor:pointer}"
|
||||||
|
".btn{width:100%;padding:10px;border:none;border-radius:6px;"
|
||||||
|
"font-size:.88em;cursor:pointer;margin-top:5px;font-weight:bold}"
|
||||||
|
".save{background:#e94560;color:#fff}"
|
||||||
|
".save:hover{background:#c73652}"
|
||||||
|
".rst{background:#334155;color:#eee}"
|
||||||
|
".rst:hover{background:#475569}"
|
||||||
|
"</style></head><body>"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
cl.print("<header><div><h1>☀ ShineLAN-X</h1>"
|
||||||
|
"<div class='sub'>Growatt Modbus → MQTT Bridge</div></div>"
|
||||||
|
"<div style='text-align:right'><span class='badge ");
|
||||||
|
cl.print(mqtt.connected() ? "on'>Verbunden" : "off'>Getrennt");
|
||||||
|
cl.print("</span><div class='sub'>IP: ");
|
||||||
|
cl.print(Ethernet.localIP());
|
||||||
|
cl.print("</div></div></header><main>");
|
||||||
|
|
||||||
|
// Statistik
|
||||||
|
cl.print("<div class='stats'>"
|
||||||
|
"<div class='sbox'><div class='sval'>");
|
||||||
|
cl.print(g_okTotal);
|
||||||
|
cl.print("</div><div class='slbl'>OK</div></div>"
|
||||||
|
"<div class='sbox'><div class='sval'>");
|
||||||
|
cl.print(g_failTotal);
|
||||||
|
cl.print("</div><div class='slbl'>Fehler</div></div>"
|
||||||
|
"<div class='sbox'><div class='sval'>0x");
|
||||||
|
snprintf(buf, sizeof(buf), "%02X", g_lastErr);
|
||||||
|
cl.print(buf);
|
||||||
|
cl.print("</div><div class='slbl'>Letzter Err</div></div></div>");
|
||||||
|
|
||||||
|
// Formular — Netzwerk
|
||||||
|
cl.print("<form method='post' action='/'>"
|
||||||
|
"<div class='card'><h2>Netzwerk</h2>"
|
||||||
|
"<div class='row'><label>DHCP</label>"
|
||||||
|
"<input type='checkbox' id='dc' name='dhcp'");
|
||||||
|
if (cfg.useDhcp) cl.print(" checked");
|
||||||
|
cl.print(" onchange='tog()'></div><div id='st'>");
|
||||||
|
|
||||||
|
ipToStr(cfg.ip, buf);
|
||||||
|
cl.print("<div class='row'><label>IP-Adresse</label>"
|
||||||
|
"<input type='text' name='ip' value='"); cl.print(buf); cl.print("'></div>");
|
||||||
|
ipToStr(cfg.gw, buf);
|
||||||
|
cl.print("<div class='row'><label>Gateway</label>"
|
||||||
|
"<input type='text' name='gw' value='"); cl.print(buf); cl.print("'></div>");
|
||||||
|
ipToStr(cfg.sn, buf);
|
||||||
|
cl.print("<div class='row'><label>Subnet</label>"
|
||||||
|
"<input type='text' name='sn' value='"); cl.print(buf); cl.print("'></div>");
|
||||||
|
ipToStr(cfg.dns, buf);
|
||||||
|
cl.print("<div class='row'><label>DNS</label>"
|
||||||
|
"<input type='text' name='dns' value='"); cl.print(buf); cl.print("'></div>");
|
||||||
|
cl.print("</div></div>");
|
||||||
|
|
||||||
|
// Formular — MQTT
|
||||||
|
cl.print("<div class='card'><h2>MQTT</h2>"
|
||||||
|
"<div class='row'><label>Broker</label>"
|
||||||
|
"<input type='text' name='broker' value='");
|
||||||
|
cl.print(cfg.mqttBroker);
|
||||||
|
cl.print("'></div>"
|
||||||
|
"<div class='row'><label>Port</label>"
|
||||||
|
"<input type='number' name='port' value='");
|
||||||
|
cl.print(cfg.mqttPort);
|
||||||
|
cl.print("' min='1' max='65535'></div>"
|
||||||
|
"<div class='row'><label>Benutzer</label>"
|
||||||
|
"<input type='text' name='user' value='");
|
||||||
|
cl.print(cfg.mqttUser);
|
||||||
|
cl.print("' autocomplete='off'></div>"
|
||||||
|
"<div class='row'><label>Passwort</label>"
|
||||||
|
"<input type='password' name='pass' value='");
|
||||||
|
cl.print(cfg.mqttPass);
|
||||||
|
cl.print("' autocomplete='off'></div>"
|
||||||
|
"<div class='row'><label>Intervall (s)</label>"
|
||||||
|
"<input type='number' name='interval' value='");
|
||||||
|
cl.print(cfg.updateMs / 1000UL);
|
||||||
|
cl.print("' min='1' max='3600'></div></div>");
|
||||||
|
|
||||||
|
// Buttons + Script
|
||||||
|
cl.print(
|
||||||
|
"<button class='btn save' type='submit'>"
|
||||||
|
"💾 Speichern & Neustart</button>"
|
||||||
|
"<button class='btn rst' type='button' "
|
||||||
|
"onclick='if(confirm(\"Wirklich auf Werkseinstellungen zur\\u00FCcksetzen?\"))"
|
||||||
|
"location=\"/reset\"'>↻ Werkseinstellungen</button>"
|
||||||
|
"</form></main>"
|
||||||
|
"<script>"
|
||||||
|
"function tog(){"
|
||||||
|
"document.getElementById('st').style.display="
|
||||||
|
"document.getElementById('dc').checked?'none':'block';}"
|
||||||
|
"window.onload=tog;"
|
||||||
|
"</script></body></html>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// MQTT
|
||||||
@@ -85,11 +423,8 @@ void publishDiscovery() {
|
|||||||
|
|
||||||
for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
|
for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
|
||||||
const Sensor& s = SENSORS[i];
|
const Sensor& s = SENSORS[i];
|
||||||
|
|
||||||
snprintf(topic, sizeof(topic),
|
snprintf(topic, sizeof(topic),
|
||||||
"homeassistant/sensor/%s_%s/config",
|
"homeassistant/sensor/%s_%s/config", DEVICE_ID, s.id);
|
||||||
DEVICE_ID, s.id);
|
|
||||||
|
|
||||||
snprintf(payload, sizeof(payload),
|
snprintf(payload, sizeof(payload),
|
||||||
"{"
|
"{"
|
||||||
"\"name\":\"%s\","
|
"\"name\":\"%s\","
|
||||||
@@ -110,7 +445,6 @@ void publishDiscovery() {
|
|||||||
s.id,
|
s.id,
|
||||||
s.unit, s.deviceClass, s.stateClass, s.icon,
|
s.unit, s.deviceClass, s.stateClass, s.icon,
|
||||||
DEVICE_ID, DEVICE_NAME, DEVICE_MODEL, DEVICE_MFR);
|
DEVICE_ID, DEVICE_NAME, DEVICE_MODEL, DEVICE_MFR);
|
||||||
|
|
||||||
mqtt.publish(topic, payload, true);
|
mqtt.publish(topic, payload, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,8 +452,8 @@ void publishDiscovery() {
|
|||||||
bool mqttReconnect() {
|
bool mqttReconnect() {
|
||||||
DebugSerial.print("MQTT connecting... ");
|
DebugSerial.print("MQTT connecting... ");
|
||||||
ledSet(LED_RED, true);
|
ledSet(LED_RED, true);
|
||||||
bool ok = (strlen(MQTT_USER) > 0)
|
bool ok = (strlen(cfg.mqttUser) > 0)
|
||||||
? mqtt.connect(MQTT_CLIENT, MQTT_USER, MQTT_PASSWORD)
|
? mqtt.connect(MQTT_CLIENT, cfg.mqttUser, cfg.mqttPass)
|
||||||
: mqtt.connect(MQTT_CLIENT);
|
: mqtt.connect(MQTT_CLIENT);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
DebugSerial.println("OK");
|
DebugSerial.println("OK");
|
||||||
@@ -142,55 +476,55 @@ void setup() {
|
|||||||
DebugSerial.println("\r\n=== Growatt ShineLAN-X ===");
|
DebugSerial.println("\r\n=== Growatt ShineLAN-X ===");
|
||||||
DebugSerial.println("Build: " __DATE__ " " __TIME__);
|
DebugSerial.println("Build: " __DATE__ " " __TIME__);
|
||||||
|
|
||||||
// LEDs initialisieren (aktiv LOW laut Referenz)
|
configLoad(cfg);
|
||||||
|
|
||||||
pinMode(LED_DEBUG, OUTPUT); ledSet(LED_DEBUG, false);
|
pinMode(LED_DEBUG, OUTPUT); ledSet(LED_DEBUG, false);
|
||||||
pinMode(LED_RED, OUTPUT); ledSet(LED_RED, false);
|
pinMode(LED_RED, OUTPUT); ledSet(LED_RED, false);
|
||||||
pinMode(LED_GREEN, OUTPUT); ledSet(LED_GREEN, false);
|
pinMode(LED_GREEN, OUTPUT); ledSet(LED_GREEN, false);
|
||||||
pinMode(LED_BLUE, OUTPUT); ledSet(LED_BLUE, false);
|
pinMode(LED_BLUE, OUTPUT); ledSet(LED_BLUE, false);
|
||||||
|
|
||||||
// Startup-Blink: alle LEDs kurz an
|
|
||||||
ledSet(LED_RED, true); ledSet(LED_GREEN, true); ledSet(LED_BLUE, true);
|
ledSet(LED_RED, true); ledSet(LED_GREEN, true); ledSet(LED_BLUE, true);
|
||||||
delay(300);
|
delay(300);
|
||||||
ledSet(LED_RED, false); ledSet(LED_GREEN, false); ledSet(LED_BLUE, false);
|
ledSet(LED_RED, false); ledSet(LED_GREEN, false); ledSet(LED_BLUE, false);
|
||||||
|
|
||||||
// ENC28J60 Reset
|
|
||||||
DebugSerial.println("ETH: reset...");
|
|
||||||
pinMode(ETH_RST_PIN, OUTPUT);
|
pinMode(ETH_RST_PIN, OUTPUT);
|
||||||
digitalWrite(ETH_RST_PIN, LOW); delay(20);
|
digitalWrite(ETH_RST_PIN, LOW); delay(20);
|
||||||
digitalWrite(ETH_RST_PIN, HIGH); delay(200);
|
digitalWrite(ETH_RST_PIN, HIGH); delay(200);
|
||||||
|
|
||||||
Ethernet.init(ETH_CS_PIN);
|
Ethernet.init(ETH_CS_PIN);
|
||||||
DebugSerial.println("ETH: begin...");
|
|
||||||
|
|
||||||
#if USE_DHCP
|
if (cfg.useDhcp) {
|
||||||
|
DebugSerial.println("ETH: DHCP...");
|
||||||
if (Ethernet.begin(mac) == 0) {
|
if (Ethernet.begin(mac) == 0) {
|
||||||
DebugSerial.println("ETH: DHCP failed, reboot");
|
// DHCP fehlgeschlagen: Fallback auf gespeicherte statische IP
|
||||||
ledSet(LED_RED, true);
|
DebugSerial.println("ETH: DHCP failed, static fallback");
|
||||||
delay(3000);
|
IPAddress ip (cfg.ip[0], cfg.ip[1], cfg.ip[2], cfg.ip[3]);
|
||||||
NVIC_SystemReset();
|
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]);
|
||||||
#else
|
IPAddress dns(cfg.dns[0], cfg.dns[1], cfg.dns[2], cfg.dns[3]);
|
||||||
IPAddress ip(STATIC_IP);
|
|
||||||
IPAddress gw(STATIC_GW);
|
|
||||||
IPAddress sn(STATIC_SUBNET);
|
|
||||||
IPAddress dns(STATIC_DNS);
|
|
||||||
Ethernet.begin(mac, ip, dns, gw, sn);
|
Ethernet.begin(mac, ip, dns, gw, sn);
|
||||||
#endif
|
}
|
||||||
|
} 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.print("ETH: IP=");
|
||||||
DebugSerial.println(Ethernet.localIP());
|
DebugSerial.println(Ethernet.localIP());
|
||||||
DebugSerial.print("ETH: link=");
|
|
||||||
DebugSerial.println(Ethernet.linkStatus() == LinkON ? "UP" : "DOWN");
|
|
||||||
|
|
||||||
if (Ethernet.linkStatus() == LinkON) ledSet(LED_DEBUG, true);
|
if (Ethernet.linkStatus() == LinkON) ledSet(LED_DEBUG, true);
|
||||||
|
|
||||||
// MQTT
|
webServer.begin();
|
||||||
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
|
DebugSerial.print("Web: http://");
|
||||||
|
DebugSerial.println(Ethernet.localIP());
|
||||||
|
|
||||||
|
mqtt.setServer(cfg.mqttBroker, cfg.mqttPort);
|
||||||
mqtt.setBufferSize(768);
|
mqtt.setBufferSize(768);
|
||||||
|
|
||||||
// Modbus: USB-CDC (PA11/PA12) — wird aktiviert sobald Wechselrichter angeschlossen
|
SerialUSB.begin(MODBUS_BAUD);
|
||||||
// SerialUSB.begin(MODBUS_BAUD);
|
delay(10000);
|
||||||
// modbus.begin(MODBUS_ADDR, SerialUSB);
|
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
|
||||||
|
modbus.begin(MODBUS_ADDR, modbusSerial);
|
||||||
|
|
||||||
DebugSerial.println("Setup done.");
|
DebugSerial.println("Setup done.");
|
||||||
}
|
}
|
||||||
@@ -201,6 +535,8 @@ void setup() {
|
|||||||
unsigned long lastUpdate = 0;
|
unsigned long lastUpdate = 0;
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
handleWebRequest();
|
||||||
|
|
||||||
if (!mqtt.connected()) {
|
if (!mqtt.connected()) {
|
||||||
if (!mqttReconnect()) {
|
if (!mqttReconnect()) {
|
||||||
delay(5000);
|
delay(5000);
|
||||||
@@ -208,33 +544,66 @@ void loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mqtt.loop();
|
mqtt.loop();
|
||||||
|
handleWebRequest();
|
||||||
|
|
||||||
if (millis() - lastUpdate < UPDATE_INTERVAL) return;
|
if (millis() - lastUpdate < cfg.updateMs) return;
|
||||||
lastUpdate = millis();
|
lastUpdate = millis();
|
||||||
|
|
||||||
ledSet(LED_BLUE, true);
|
ledSet(LED_BLUE, true);
|
||||||
|
|
||||||
char stateTopic[64];
|
bool usbReady = (SerialUSB.availableForWrite() == 127);
|
||||||
char valueStr[16];
|
uint8_t okCount = 0, failCount = 0;
|
||||||
|
uint8_t lastErr = 0;
|
||||||
|
uint16_t lastErrReg = 0;
|
||||||
|
|
||||||
for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
|
for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
|
||||||
|
mqtt.loop();
|
||||||
|
handleWebRequest();
|
||||||
|
|
||||||
const Sensor& s = SENSORS[i];
|
const Sensor& s = SENSORS[i];
|
||||||
|
|
||||||
|
if (!usbReady) {
|
||||||
|
failCount++; lastErr = 0xFE; lastErrReg = s.address;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t result;
|
uint8_t result;
|
||||||
uint32_t raw = 0;
|
uint32_t raw = 0;
|
||||||
|
|
||||||
// TODO: Modbus aktivieren sobald USB-CDC (Wechselrichter) angeschlossen
|
if (s.isDword) {
|
||||||
result = ModbusMaster::ku8MBSuccess; // Platzhalter
|
result = modbus.readInputRegisters(s.address, 2);
|
||||||
raw = 0;
|
if (result == ModbusMaster::ku8MBSuccess)
|
||||||
(void)result;
|
raw = ((uint32_t)modbus.getResponseBuffer(0) << 16) | modbus.getResponseBuffer(1);
|
||||||
|
} else {
|
||||||
|
result = modbus.readInputRegisters(s.address, 1);
|
||||||
|
if (result == ModbusMaster::ku8MBSuccess)
|
||||||
|
raw = modbus.getResponseBuffer(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != ModbusMaster::ku8MBSuccess) {
|
||||||
|
failCount++; lastErr = result; lastErrReg = s.address;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
okCount++;
|
||||||
float value = raw * s.scale;
|
float value = raw * s.scale;
|
||||||
|
char valueStr[16];
|
||||||
dtostrf(value, 1, (s.scale < 0.1f) ? 2 : 1, valueStr);
|
dtostrf(value, 1, (s.scale < 0.1f) ? 2 : 1, valueStr);
|
||||||
|
|
||||||
|
char stateTopic[64];
|
||||||
snprintf(stateTopic, sizeof(stateTopic), "growatt/shinelan/%s", s.id);
|
snprintf(stateTopic, sizeof(stateTopic), "growatt/shinelan/%s", s.id);
|
||||||
mqtt.publish(stateTopic, valueStr);
|
mqtt.publish(stateTopic, valueStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g_okTotal += okCount;
|
||||||
|
g_failTotal += failCount;
|
||||||
|
if (lastErr) g_lastErr = lastErr;
|
||||||
|
|
||||||
|
char dbg[64];
|
||||||
|
snprintf(dbg, sizeof(dbg), "ok=%u fail=%u err=0x%02X@%u av=%d rx=%d",
|
||||||
|
okCount, failCount, lastErr, lastErrReg,
|
||||||
|
SerialUSB.availableForWrite(), SerialUSB.available());
|
||||||
|
mqtt.publish("growatt/shinelan/debug", dbg);
|
||||||
|
|
||||||
ledSet(LED_BLUE, false);
|
ledSet(LED_BLUE, false);
|
||||||
DebugSerial.println("Update done.");
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user