Neuer Baustein zum Parsen von JSON-Streams

Hallo,

wie beim neuen Wetterbaustein für Weatherbit.io bereits erwähnt (siehe Neuer Wetter-Baustein für Weatherbit.io) habe ich einen neuen Bausteinen für das Parsen von JSON-Streams geschrieben. Gedankliche Vorlage war der XML_READER, der Aufbau und die Ansteuerung unterscheiden sich allerdings doch ziemlich gegenüber diesem.

Ich rechne nicht damit, dass die breite Masse Verwendung für einen solchen Baustein hat, daher spare ich mir eine genauere Beschreibung an dieser Stelle. Sollte hierfür Bedarf bestehen, bitte einfach melden, dann liefere ich sie gerne nach.

peewit, du könntest dir überlegen, ob der Bausteinen einen Platz in einer neuen Version der network.lib finden soll, dann sinnvollerweise gemeinsam mit dem Weatherbit-Baustein. Die Doku kann ich dann gerne übernehmen; die Portierung auf Steuerungssysteme abseits von CODESYS müssten dann andere Mitglieder der OSCAT-Community übernehmen, die dafür Verwendung haben.

Der JSON_READER ist als CODESYS V2.3/TwinCAT 2-Export angehängt. Eine CODESYS V3.5-Variante kann ich gerne bei Bedarf nachliefern.

Cheers,
mattsches

hallo mattsches

inwieweit ist den diese wetterauswertung von dauer.. ?
alle bisherigen haben sie uns irgendwann abgedreht…

die bausteine werden ich langfristig sicherlich integrieren
wenn ich was brauche werde ich mich bei dir melden

die portierungen mache ich auch gerne

Tja, das ist natürlich eine gute Frage. Mit Openweathermap hat es ja nicht allzu lange geklappt. Mal schauen, wie es bei Weatherbit aussieht.

Das Gute ist, dass mit dem JSON_PARSER nun eine größere Zahl von Wetter-APIs genutzt werden kann, da immer mehr Anbieter auf JSON umzusteigen scheinen. Einen solchen neuen Dienst anzuzapfen, ist ja dann keine große Sache mehr.

Hallo,

also was macht ihr mit den Wetterdaten wenn ihr diese habt?? Was kann man Sinnvolles damit anfangen?

  • Am HMI anzeigen (ok, das ist profan).
  • Die Jalousien früher zum Verschatten schließen, wenn höhere Temperaturen angekündigt sind.
  • Die Vorlauftemperatur der Heizung drosseln bei angekündigtem Sonnenschein (mache ich nicht, macht meine Heizung schon von selbst).

Hallo mattsches,

Würde mir gerne mal deinen Baustein ansehen
Wäre super wenn du mir den Baustein in der CODESYS V3.5-Variante bereit stellen könntest
Bei mir Zuhause läuft eine Beckhoff CPU mit TwinCAT3 und ich würde mir gerne einen Baustein bauen mit dem ich meinen Fronius Wechselrichter
via JSON auslesen kann

An Wetterdaten wäre ich natürlich auch interessiert

Danke

Hallo,

ich komme gerade nicht dazu, den Baustein auf 3.5 zu portieren. Aber du müsstest die Exportdatei eigentlich ganz gut importieren können. In der Regel passen dann die Referenzen auf die aufgerufenen Bausteine nicht, weil in 3.5 mit Namensraum gearbeitet wird (.<FB_Name>), was in 2.3 nicht der Fall war.

Gruß,
mattsches

Hat jemand den Reader im Einsatz ?
Ich bekomme ihn nicht zum Laufen um meine Wärmepumpe auszulesen.
Danke.

Gruß Thorsten

Der Baustein werkelt im Wetterbaustein für Weatherbit.io. Siehe Link im ersten Post.

Update, falls jemand außer mir den Baustein im Einsatz haben sollte: Das gute Stück kam nicht mit negativen Werte und leeren Objekten ( {} ) klar, wie mir erst jetzt aufgefallen ist, wo ich ebenfalls einen Fronius-Wechselrichter damit anzapfen wollte. Anbei eine überarbeitete Version, mit der das bei mir nun funktioniert. Ursprünglich wollte ich eine Datei anhängen, doch die Funktion hier im Forum scheint abgedreht worden zu sein. Daher etwas umständlicher als Quelltext:

Deklaration:


FUNCTION_BLOCK JSON_READER
VAR CONSTANT
	MAX_LEVEL : UINT := 19;
END_VAR

VAR_INPUT
	RST : BOOL := TRUE;					(* reset *)
	WATCHDOG : TIME := t#1ms;			(* watchdog time *)
	START_POS : UINT := 0;				(* first byte in network buffer to be processed *)
	STOP_POS : UINT := 0;				(* last byte in network buffer to be processed *)
	BUF : NW_BUF_LONG;					(* network buffer that contains the JSON string as byte stream *)
END_VAR

VAR_OUTPUT
	COUNT : UINT := 0;					(* number of parsed JSON elements *)
	LEVEL : UINT := 0;					(* nesting level of current JSON element *)
	ARRAY_INDEX : ARRAY[1..MAX_LEVEL] OF INT := MAX_LEVEL(-1);	(* current array index value (-1: element is not part of an array) *)
	PATH : STRING(STRING_LENGTH);		(* full path of current JSON element *)
	ELEMENT : STRING(STRING_LENGTH);	(* name of current JSON element *)
	VALUE : STRING(STRING_LENGTH);		(* value of current JSON element *)
	NEW_DATA : BOOL := FALSE;			(* new data is available (next element has been parsed *)
	DONE : BOOL := FALSE;				(* last element has been parsed *)
	ERROR : BOOL := FALSE;				(* error occured, see status for details; reset is required *)
	STATUS : BYTE := 0;					(* ESR compatible status output ( 0=ok,
																		  1=error: dot not found in path string,
																		  2=error: closing square bracket not found in path string,
																		  3=max. nesting level exceeded,
																		100=element has been parsed, 
																		101=watchdog timer expired *)
END_VAR

VAR
	CurrentByte : UINT;					(* index of currently scanned byte in network buffer *)
	ObjectEmpty : BOOL := FALSE;		(* current object is still empty *)
	LookForValue : BOOL := FALSE;		(* colon has been found, now look for value *)
	FirstByte : UINT := 0;				(* first byte of string to be extracted from buffer *)
	LastByte : UINT := 0;				(* last byte of string to be extracted from buffer *)
	ElementParsed : BOOL := FALSE;		(* element has been parsed *)
	PathOverflow : BOOL := FALSE;		(* maximum string length exceeded for path variable *)

	StringFound : INT := 0;				(* position of string within another string *)
	LoopCount : INT := 0;				(* loop counter *)
	WatchdogTimer : TON;				(* watchdog timer *)

	_Level : UINT := 0;					(* nesting level of current JSON element *)
	_Path : STRING(STRING_LENGTH);		(* full path of current JSON element *)
	_ArrayIndex : ARRAY[1..MAX_LEVEL] OF INT := MAX_LEVEL(-1);	(* current array index value (-1: element is not part of an array) *)
	_Element : STRING(STRING_LENGTH);	(* name of current JSON element *)
	_Value : STRING(STRING_LENGTH);		(* value of current JSON element *)

	pValueString : POINTER TO ARRAY[0..STRING_LENGTH] OF BYTE;
END_VAR


(*

version 1.1		11 oct 2022
programmer 	md
tested by	md

JSON_READER allows to parse a JSON string stored in a byte array (buffer) and extract the JSON elements one after another.

The parsing is monitored by a watchdog timer such that the block does cause a delay of the PLC program of more than the
configured time. The block further automatically interrupts the parsing as soon as an element and its value have been found. 
It then provides both at the CTRL structured variable for external processing and continues to search for the next element
when it is called again (next cycle). 

Values are always provided as strings, conversion to numeric data types needs to be done by the calling program.

*)


Code:


(* reset/initialize *)
IF RST THEN
	CurrentByte := START_POS;
	ObjectEmpty := FALSE;
	LookForValue := FALSE;
	FirstByte := 0;
	LastByte := 0;

	FOR LoopCount := 1 TO MAX_LEVEL DO
		_ArrayIndex[LoopCount] := -1;
	END_FOR

	ElementParsed := FALSE;
	PathOverflow := FALSE;

	_Level := 0;
	_Element := '';
	_Path := '';
	_Value := '';

	COUNT := 0;
	LEVEL := 0;
	ELEMENT := '';
	PATH := '';
	VALUE := '';
	NEW_DATA := FALSE;
	DONE := FALSE;
	ERROR := FALSE;
	STATUS := 0;
END_IF;


(* reset watchdog timer *)
WatchdogTimer(IN:=FALSE , PT:= WATCHDOG, Q=> , ET=> );

(* reset new data flag *)
NEW_DATA := FALSE;

(* loop through buffer until buffer end reached OR element has been parsed OR watchdog timer has expired *)
WHILE (CurrentByte <= STOP_POS AND NOT ElementParsed AND NOT WatchdogTimer.Q AND NOT ERROR AND NOT RST AND NOT DONE) DO

	WatchdogTimer(IN:=TRUE , PT:= WATCHDOG, Q=> , ET=> );		(* call watchdog timer *)

	CASE BUF[CurrentByte] OF	(* evaluate current character *)

	0:								(* end of string *)
		DONE := TRUE;

	123: 							(* '{' object start *)
		IF _Level < MAX_LEVEL THEN
			_Level := _Level + 1;
			ObjectEmpty := TRUE;
			LookForValue := FALSE;
			FirstByte := 0;
			_Element := '';
			_Value := '';
		ELSE
			ERROR := TRUE;
			STATUS := 3;
		END_IF

	125: 							(* '}' object end *)
		IF LookForValue AND _Value <> '' THEN	(* element has been parsed *)
			ElementParsed := TRUE;
			EXIT;									(* exit loop immediately in order to first copy the element details to the output parameters *)
		END_IF

		IF _Level > 1 AND NOT PathOverflow THEN
			_Element := '';
			_Value := '';
			_Level := _Level - 1;					(* up one level *)
			LookForValue := FALSE;					(* next string must be a key, not a value *)
			FirstByte := 0;

			IF NOT ObjectEmpty THEN					(* don't change path if current object is still empty, i. e. nothing has been appended to the path before *)
				StringFound := FINDB(_Path, '.');
				IF StringFound > 1 THEN
					_Path := LEFT(_Path, StringFound - 1);	(* remove last element from path string *)
				ELSE									(* error: dot has not been found *)
					ERROR := TRUE;
					STATUS := 1;
				END_IF
			END_IF

			ObjectEmpty := FALSE;
		END_IF

	91: 							(* '[' array start *)
		IF LEN(_Path) + 3 < STRING_LENGTH AND NOT PathOverflow THEN
			_Path := CONCAT(_Path, '[0]');						(* append array index to path *)
		ELSE
			_Path := 'OVERFLOW';
			PathOverflow := TRUE;
		END_IF
		_Element := CONCAT(_Element, '[0]');				(* append array index to path *)
		_ArrayIndex[_Level] := 0;							(* array index *)

	93: 							(* ']' array end *)
		IF LookForValue AND _Value <> '' THEN	(* element has been parsed *)
			ElementParsed := TRUE;
			EXIT;									(* exit loop immediately in order to first copy the element details to the output parameters *)
		END_IF

		IF NOT PathOverflow THEN
			StringFound := FINDB(_Path, '[');
			IF StringFound > 1 THEN					(* remove array index from path string *)
				_Path := LEFT(_Path, StringFound - 1);
				_Element := LEFT(_Element, FINDB(_Path, '[') - 1);
				_Value := '';
				_ArrayIndex[_Level] := -1;							(* reset array index *)
			ELSE									(* error: opening square bracket has not been found *)
				ERROR := TRUE;
				STATUS := 2;
			END_IF
		END_IF


	58: 							(* ':' key delimiter *)
		LookForValue := TRUE;

	44:								(* ',' element delimiter *)
		IF LookForValue AND _Value <> '' THEN	(* element has been parsed *)
			ElementParsed := TRUE;
			EXIT;									(* exit loop immediately in order to first copy the element details to the output parameters *)
		END_IF

		IF RIGHT(_Path, 1) = ']' THEN	(* previous element is part of an array *)
			IF LEN(LEFT(_Path, FINDB(_Path, '['))) + LEN(INT_TO_STRING(_ArrayIndex[_Level] + 1)) + 1 <= STRING_LENGTH THEN
				_ArrayIndex[_Level] := _ArrayIndex[_Level] + 1;	(* increment array index *)
				_Path := LEFT(_Path, FINDB(_Path, '['));
				_Path := CONCAT(_Path, INT_TO_STRING(_ArrayIndex[_Level]));
				_Path := CONCAT(_Path, ']');
			ELSE
				_Path := 'OVERFLOW';
				PathOverflow := TRUE;
			END_IF
		ELSE										(* previous element is not part of an array *)
			StringFound := FINDB(_Path, '.');
			IF StringFound > 1 THEN						(* remove last element from path string *)
				_Path := LEFT(_Path, StringFound - 1);
			ELSE										(* previous element was root, empty path variable *)
				_Path := '';
			END_IF
			_Element := '';
			_Value := '';
		END_IF

	34: 							(* '"' quotation mark *)
		IF FirstByte = 0 THEN
			FirstByte := CurrentByte + 1;
		ELSE
			IF NOT LookForValue THEN		(* key has been parsed *)
				ObjectEmpty := FALSE;
				_Element := BUFFER_TO_STRING(PT:=ADR(BUF),SIZE:=STOP_POS + 1,START:=FirstByte,STOP:=CurrentByte - 1);
				IF _Path = '' THEN				(* path empty, use element as root for path *)
					_Path := _Element;
				ELSE							(* append key to path *)
					IF LEN(_Path) + LEN(_Element) + 1 <= STRING_LENGTH THEN
						_Path := CONCAT(_Path, '.');
						_Path := CONCAT(_Path, _Element);
					ELSE
						_Path := 'OVERFLOW';
						PathOverflow := TRUE;
					END_IF
				END_IF
			ELSE							(* value has been parsed *)
				_Value := BUFFER_TO_STRING(PT:=ADR(BUF),SIZE:=STOP_POS + 1,START:=FirstByte,STOP:=CurrentByte - 1);
				ElementParsed := TRUE;
			END_IF
			FirstByte := 0;
		END_IF

	45, 48..57, 46, 110, 117, 108:				(* minus, numbers and dot + characters for "null" (not as string but as null value *)
		IF LookForValue AND FirstByte = 0 THEN
			pValueString := ADR(_Value);
			pValueString^[LEN(_Value) + 1] := 0;
			pValueString^[LEN(_Value)] := BUF[CurrentByte];
		END_IF

	END_CASE
	CurrentByte := CurrentByte + 1;			(* proceed to next byte *)

END_WHILE

IF ElementParsed THEN
	ElementParsed := FALSE;
	LookForValue := FALSE;
	COUNT := COUNT + 1;	(* copy output values to interface *)
	LEVEL := _Level;
	PATH := _Path;
	ARRAY_INDEX := _ArrayIndex;
	ELEMENT := _Element;
	VALUE := _Value;
	NEW_DATA := TRUE;
	STATUS := 100;
END_IF

IF CurrentByte > STOP_POS THEN		(* end of string to be parsed reached *)
	DONE := TRUE;
ELSIF WatchdogTimer.Q THEN
	STATUS := 101;
END_IF


(* revision history
md	10 Jul 2019	rev 1.0
	original version

md	11 Oct 2022 rev 1.1
	fixed negative values not being recognized
	fixed empty objects breaking path handling
*)

Wie cool wäre es, das über Github machen zu können…

Hallo mattsches,
Vorab sorry, aber da ich kein Deutsch spreche, benutze ich einen automatischen Übersetzer :wink:

Ich habe gerade den Funktionsblock “JSON_READER” kopiert und es funktioniert. Ich erhalte sukzessive die Daten eines JSON-Objekts. Vielen Dank für diese Arbeit!
Ich bin mir allerdings nicht sicher, ob ich weiß, wie ich das Modul verwenden soll. Haben Sie vielleicht ein Beispiel für eine POU gesehen, die dieses Modul verwendet?
Vielen Dank im Voraus.

Hallo, nutze schon lange Oscat Bausteine, danke dafür erstmal. Ich bräuchte Unterstützung um den obigen Code des Json Parsers in PCWORX umzuschreiben. Gerade bei der Pointer-Geschichte komme ich nicht weiter.
Möchte eine Panasonic Wärmepumpe über Heishamon mit Http auslesen und bekomme ein Json zurück.
Danke