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:
@@ -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);
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user