HAOS Add-on: MVP + NuttX Binary + .gitignore

- haos-addon/: vollständiges HA Add-on (config.yaml, Dockerfile, build.yaml)
  - Python Backend: pymodbus Modbus TCP → paho-mqtt MQTT Discovery
  - Unterstützte Modelle: MIC 1500/2000 TL-X, SPH 5000 TL3, MOD 6000 TL3
  - Web UI: Wechselrichter-Auswahl, Modbus/MQTT-Konfig, Live-Sensor-Grid (dark theme)
  - MQTT HA Discovery für alle Sensoren mit device_class, state_class, icon
- ShineLAN-X/releases/nuttx-mbusd-shinelanx.bin: NuttX Firmware (ohne DFU, 0x08000000)
- .gitignore: Logs, MQTT-JSON, shinelanx-modbus/ ausgeschlossen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-04-24 22:30:45 +02:00
parent 4d2da56baf
commit 0d6b860664
12 changed files with 1327 additions and 47 deletions
+149 -47
View File
@@ -63,16 +63,65 @@ extern "C" void USBD_reenumerate(void) {
HardwareSerial DebugSerial(PA10, PA9);
// USBSerial::flush() wartet endlos wenn der Wechselrichter den IN-Endpoint nicht pollt.
// Dieser Wrapper ersetzt flush() durch eine No-Op.
// 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:
int available() override { return SerialUSB.available(); }
int read() override { return SerialUSB.read(); }
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 { return SerialUSB.write(b); }
size_t write(const uint8_t* buf, size_t size) override { return SerialUSB.write(buf, size); }
void flush() override {}
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;
@@ -153,6 +202,7 @@ 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
@@ -524,6 +574,12 @@ void setup() {
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.");
@@ -533,6 +589,7 @@ void setup() {
// Loop
// ============================================================
unsigned long lastUpdate = 0;
static bool g_usbInfoPublished = false;
void loop() {
handleWebRequest();
@@ -546,65 +603,110 @@ void loop() {
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);
uint8_t avStart = SerialUSB.availableForWrite();
bool usbReady = (avStart >= 8);
uint8_t okCount = 0, failCount = 0;
uint8_t lastErr = 0;
uint32_t okCnt = 0, failCnt = 0;
uint8_t lastErr = 0;
uint16_t lastErrReg = 0;
for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
mqtt.loop();
handleWebRequest();
const Sensor& s = SENSORS[i];
modbusSerial.reset();
if (!usbReady) {
failCount++; lastErr = 0xFE; lastErrReg = s.address;
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;
}
uint8_t result;
uint32_t raw = 0;
okCnt++;
float val;
if (s.isDword) {
result = modbus.readInputRegisters(s.address, 2);
if (result == ModbusMaster::ku8MBSuccess)
raw = ((uint32_t)modbus.getResponseBuffer(0) << 16) | modbus.getResponseBuffer(1);
uint32_t raw = ((uint32_t)modbus.getResponseBuffer(0) << 16)
| modbus.getResponseBuffer(1);
val = raw * s.scale;
} else {
result = modbus.readInputRegisters(s.address, 1);
if (result == ModbusMaster::ku8MBSuccess)
raw = modbus.getResponseBuffer(0);
val = modbus.getResponseBuffer(0) * s.scale;
}
if (result != ModbusMaster::ku8MBSuccess) {
failCount++; lastErr = result; lastErrReg = s.address;
continue;
}
okCount++;
float value = raw * s.scale;
char valueStr[16];
dtostrf(value, 1, (s.scale < 0.1f) ? 2 : 1, valueStr);
char stateTopic[64];
snprintf(stateTopic, sizeof(stateTopic), "growatt/shinelan/%s", s.id);
mqtt.publish(stateTopic, valueStr);
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 += okCount;
g_failTotal += failCount;
if (lastErr) g_lastErr = lastErr;
char dbg[64];
snprintf(dbg, sizeof(dbg), "ok=%u fail=%u err=0x%02X@%u avS=%d avE=%d rx=%d",
okCount, failCount, lastErr, lastErrReg,
avStart, SerialUSB.availableForWrite(), SerialUSB.available());
mqtt.publish("growatt/shinelan/debug", dbg);
g_okTotal += okCnt;
g_failTotal += failCnt;
g_lastErr = lastErr;
ledSet(LED_BLUE, false);
}