Autor Thema: Neuer Baustein zum Parsen von JSON-Streams  (Gelesen 11711 mal)

0 Mitglieder und 1 Gast betrachten dieses Thema.

Offline mattsches

  • Sr. Member
  • ****
  • Beiträge: 268
    • Profil anzeigen
Neuer Baustein zum Parsen von JSON-Streams
« am: 21. Juli 2019, 21:35:59 »
Hallo,

wie beim neuen Wetterbaustein für Weatherbit.io bereits erwähnt (siehe http://www.oscat.de/community/index.php/topic,4952.0.html) 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

Offline peewit

  • Moderator
  • *****
  • Beiträge: 2 378
    • Profil anzeigen
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #1 am: 22. Juli 2019, 19:21:12 »
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

Offline mattsches

  • Sr. Member
  • ****
  • Beiträge: 268
    • Profil anzeigen
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #2 am: 22. Juli 2019, 20:16:35 »
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.

Offline Sergej

  • Newbie
  • *
  • Beiträge: 9
    • Profil anzeigen
    • E-Mail
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #3 am: 07. Januar 2020, 13:53:15 »
Hallo,

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

Offline mattsches

  • Sr. Member
  • ****
  • Beiträge: 268
    • Profil anzeigen
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #4 am: 10. Januar 2020, 12:35:09 »
  • 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).

Offline 0skill

  • Newbie
  • *
  • Beiträge: 4
    • Profil anzeigen
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #5 am: 28. Juni 2020, 11:14:52 »
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



Offline mattsches

  • Sr. Member
  • ****
  • Beiträge: 268
    • Profil anzeigen
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #6 am: 28. Juni 2020, 11:58:31 »
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 (<Namensraum>.<FB_Name>), was in 2.3 nicht der Fall war.

Gruß,
mattsches

Offline ThorPrez

  • Newbie
  • *
  • Beiträge: 30
    • Profil anzeigen
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #7 am: 15. Mai 2021, 14:54:55 »
Hat jemand den Reader im Einsatz ?
Ich bekomme ihn nicht zum Laufen um meine Wärmepumpe auszulesen.
Danke.

Gruß Thorsten

Offline mattsches

  • Sr. Member
  • ****
  • Beiträge: 268
    • Profil anzeigen
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #8 am: 15. Mai 2021, 22:57:32 »
Der Baustein werkelt im Wetterbaustein für Weatherbit.io. Siehe Link im ersten Post.

Offline mattsches

  • Sr. Member
  • ****
  • Beiträge: 268
    • Profil anzeigen
Re: Neuer Baustein zum Parsen von JSON-Streams
« Antwort #9 am: 11. Oktober 2022, 20:41:56 »
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...