Edge-2-Edge ESS - ungültige Channel-Werte

Hallo zusammen,

ich spiele gerade mit einem in Docker simulierten Master/Slave-System herum und dabei ist mir folgendes Problem aufgefallen, zu dem ich eine Fragen hätte.

Sobald auf dem Slave die ESS deaktiviert wird, springt der Soc auf dem Master auf 65535. Also der max. Wert eines UINT16 Registers. Wenn man die Modbus Schnittstelle ausliest ist hier auch zu sehen, dass alle Bits in diesem Register auf 1 stehen. Gleiches gilt für den Channel GridMode bei der Edge-2-Edge-ESS. Dieses Problem tritt nur bei Integer-Werten auf, da diese keinen NaN-Wert haben. Float-Werte werden korrekt auf NaN gesetzt und auf dem Master die entsprechenden Channel dann mit Null beschrieben. Daher wird z.B. die ActivePower der Edge-2-Edge als Null angezeigt.

Meine Vermutung ist, dass das generell für alle Komponenten mit Integer Channels gilt, die einer Modbus-Schnittstelle zugeordnet sind, aber gleichzeitig deaktiviert sind.

Hier einige Screenshots zu der Situation:

Nun zu meiner Frage:

Würde es nicht Sinn machen, den UINT16 Wert 65535 in der OpenEMS Modbus Schnittstelle generell als ungültig anzusehen? Wenn der Master ein UINT16 Register mit 65535 erkennt, beschreibt er den Channel dann mit Null.

Ich habe das bereits testweise in der Klasse AbstractEdge2Edge mit der Funktion ElementToChannelConverter.SET_NULL_FOR_DEFAULT() umsetzen können. Ich habe aber noch keinen Pull Request erstellt, da ich gerne hier nochmal über dieses Thema sprechen würde.

Viele Grüße

Mein Testaufbau:

  • Master System
    • Simulator NRCMeterActing: Simulierter Verbrauch
    • Simulator GridMetrReacting: Simuliertes Grid Meter, was auf Verbrauch reagiert
    • Bridge Modbus/TCP: Verbindung zur Slave Edge
    • Edge-2-Edge ESS: Slave Edge
    • Controller ESS Balancing
  • Slave System:
    • Simulator EssSymmetric Reacting
    • Controller Api Modbus/TCP Read-Write: Schnittstelle für Master

In Modbus gibt es kein reserviertes „invalid value“ für Integer.
Jede Zahl ist legitim. Auch 65535 ist ein technisch gültiger UINT16-Wert — selbst wenn er in der Praxis selten gebraucht wird.

Damit würdest du bei einer generellen Regel:

UINT16=65535 ⇒ NULL

potenziell echte Messwerte unbrauchbar machen.
Das wäre silent data loss, und das ist in OpenEMS unbedingt zu vermeiden.

Ja das stimmt, das ist eine Frage, die ich mir auch selbst gestellt habe. Hättest du denn eine Idee, wie man das Problem sonst lösen könnte?

Sollte dann vielleicht die Modbus Schnittstelle so angepasst werden, dass bei einer deaktivierten Komponente garkeine Werte übermittelt werden? Dann könnte das entsprechende Register auf NULL gesetzt werden. Vorausgesetzt soetwas wie NULL gibt es in Modbus, das weiß ich leider nicht.

Ansonsten gibt es natürlich noch die Möglichkeit, dass jede Komponente selbst dafür verantwortlich ist, ungültige Werte abzufangen. Aber das finde ich nicht so schön, da das ja ein grundlegendes Problem mit Integern in der Modbus-Schnittstelle ist.

Danke für deine ausführliche Beschreibung – das Verhalten ist absolut nachvollziehbar, hat aber eine klar definierte Ursache, die nicht im Modbus-Core liegt, sondern in der Edge-2-Edge-Implementierung.

Ursache des Problems

Wenn die Slave-ESS deaktiviert wird:

  • Das Modbus-Register liefert 0xFFFF (65535) als “undefined/not available”.

  • UnsignedWordElement wandelt diesen Wert korrekt zu 65535 um.

  • In AbstractEdge2Edge.mapRemoteChannels() wird jedoch der DIRECT_1_TO_1-Converter verwendet → die 65535 wird ungefiltert an den Channel durchgereicht.

Ergebnis:

  • State of Charge: 65535 %

  • GridMode: ungültiger ENUM-Wert

  • … alle Integer-Channels ohne NaN-Semantik zeigen falsche Werte.

Warum Float-Werte funktionieren

Floats besitzen einen standardisierten NaN-Wert (IEEE 754 NaN = 0x7FC00000).
Der Modbus-Stack erkennt diesen automatisch → Channel wird null.

Integer-Typen haben keinen NaN-Wert → daher tritt das Problem ausschließlich bei UINT16/UINT32/ENUMs auf.


Vorschlag zur Lösung (ohne Änderungen am Modbus-Core)

Das Verhalten sollte nur für Edge-2-Edge korrigiert werden, denn:

  • In Modbus existiert kein universeller “invalid integer”.

  • 65535 kann in realen Protokollen ein legitimer Wert sein (SunSpec nutzt es jedoch häufig als „undefined“).

  • Eine globale Regel würde silent data loss erzeugen – unbedingt zu vermeiden.

Die Lösung liegt vollständig in der Edge-2-Edge-Komponente.


Ansatz 1: Kanal-spezifisch filtern

In AbstractEdge2Edge.mapRemoteChannels():

// bisher:
m(r.getChannelId(), element);

// neu:
m(r.getChannelId(), element,
    ElementToChannelConverter.SET_NULL_FOR_DEFAULT(0xFFFF)
);

Gilt für:

  • UINT16

  • ENUM16


Ansatz 2: Generisch nach ModbusType filtern

Eleganter und wartungsfreundlicher:

private ElementToChannelConverter getConverterForType(ModbusType type) {
    return switch (type) {
        case UINT16, ENUM16 -> SET_NULL_FOR_DEFAULT(0xFFFF);       // 65535
        case UINT32         -> SET_NULL_FOR_DEFAULT(0xFFFFFFFF);   // 4294967295
        default             -> DIRECT_1_TO_1;  // Floats/NaN bereits korrekt
    };
}

Implementierungsstelle:

io.openems.edge.edge2edge/common/AbstractEdge2Edge.java

Relevanter Ausschnitt:

if (record instanceof ModbusRecordChannel r) {
    var element = generateModbusElement(record.getType(), address);

    var converter = switch (record.getType()) {
        case UINT16, ENUM16 -> ElementToChannelConverter.SET_NULL_FOR_DEFAULT(0xFFFF);
        case UINT32 -> ElementToChannelConverter.SET_NULL_FOR_DEFAULT(0xFFFFFFFF);
        default -> ElementToChannelConverter.DIRECT_1_TO_1;
    };

    m(r.getChannelId(), element, converter);
}


Vorteile dieser Lösung

Aspekt Bewertung
Kein Core-Modbus-Change :check_mark: Nur Edge-2-Edge betroffen
Standardkonform :check_mark: SunSpec definiert 0xFFFF als „not available“
Rückwärtskompatibel :check_mark: Andere Komponenten bleiben unverändert
Keine Datenverluste :check_mark: Nur ungültige E2E-Werte werden gefiltert
Wartungsfreundlich :check_mark: Einheitliche Logik pro ModbusType

Fazit

Ein genereller „65535 = NULL“-Mechanismus im Modbus-Core wäre problematisch.
Für Edge-2-Edge hingegen ist die Null-Semantik korrekt und sinnvoll, da hier ausschließlich interne OpenEMS-Daten übertragen werden und SunSpec-ähnliche Defaults verwendet werden.

1 Like

Danke für die quasi schon fertige Lösung :smiley: Meine Frage war da nicht spezifisch genug, ich meinte die OpenEMS-interne Modbus-Schnittstelle. Aber das hast du ja gut erkannt.

Ansatz 2 gefällt mir sehr gut, damit werden direkt alle problematischen Datentypen behandelt. In der ModbusType Klasse gibts noch UINT64, den würde ich auch noch mit reinnehmen.

Wenn du die Änderung auch sinnvoll findest, würde ich dafür einen Pull-Request erstellen?

Hallo zusammen,

Wir hatten letzte Woche dasselbe Problem und haben es in unserem privaten Repo bereits gelöst, und zwar ein bisschen anders als hier vorgeschlagen.

					if (record instanceof ModbusRecordChannel r) {
						m(r.getChannelId(), element, convertUndefined(record.getType()));
					}

und der Methode

	private static ElementToChannelConverter convertUndefined(ModbusType type) {
		return switch (type) {
		case ENUM16, STRING16, FLOAT32, FLOAT64 -> DIRECT_1_TO_1;
		case UINT16 -> new ElementToChannelConverter(value -> {
			var intValue = (int) value;
			if (Arrays.equals(ModbusRecordUint16.toByteArray((short) intValue), ModbusRecordUint16.UNDEFINED_VALUE)) {
				return null;
			}
			return value;
		});
		case UINT32 -> new ElementToChannelConverter(value -> {
			var longValue = (long) value;
			if (Arrays.equals(ModbusRecordUint32.toByteArray((int) longValue), ModbusRecordUint32.UNDEFINED_VALUE)) {
				return null;
			}
			return value;
		});
		case UINT64 -> new ElementToChannelConverter(value -> {
			if (Arrays.equals(ModbusRecordUint64.toByteArray((long) value), ModbusRecordUint64.UNDEFINED_VALUE)) {
				return null;
			}
			return value;
		});
		};
	}

An eurer Lösung gefällt mir gut, dass ihr den Converter SET_NULL_FOR_DEFAULT benutzt, den kannte ich noch nicht. Allerdings habe ich es gerade getestet und es hat bei einem uint32 nicht geklappt und folgender Fehler kam im Log:

Parsing Response failed. IllegalArgumentException: Conversion for [UINT32_TEST] failed

Das habe ich nicht verstanden, mir aber auch nicht näher angeguckt.

An meiner Lösung gefällt mir, dass man sich direkt an den Wert ModbusRecordUintxx.UNDEFINED_VALUE hängt, allerdings finde ich das zweimalige Type Casting sehr unelegant. Deshalb finde ich insgesamt eure Lösung besser, aber verstehe nicht, warum ich da einen Fehler bekomme…

Eine kleine Bemerkung noch: Bei einem ENUM16 kannst du denke ich auch DIRECT_1_TO_1 benutzen, da auch ein nicht im Enum definierter Wert auf UNDEFINED gemappt werden sollte. Macht aber wahrscheinlich keinen großen Unterschied.

Viele Grüße,
Thomas

1 Like

Debuggen bitte - das kann ich dir leider nicht sagen :see_no_evil_monkey:

Hat ein bisschen gedauert, aber ich habe jetzt einen Pull-Request erstellt: Edge2Edge: filter undefined integer modbus values by tcdrop · Pull Request #3479 · OpenEMS/openems · GitHub