From efaa449552365839ecd7d9fc564997baa9055953 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Tue, 22 Mar 2022 13:09:41 +0100 Subject: [PATCH 01/27] Update libconnection --- libconnection | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libconnection b/libconnection index 0e4de4a..a1fdb4a 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit 0e4de4a5cfafec76095f4b19f425f64967fdef2b +Subproject commit a1fdb4aff448b1c9a25998bb81ea13c9da32ee46 From 4892f62211374cc2c293ee032c6aa1bc49b9355c Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Tue, 22 Mar 2022 13:10:22 +0100 Subject: [PATCH 02/27] Allow to skip prefix bytes before ID check --- libxcm/Message.cs | 6 ++++++ xcm.xsd | 1 + xcmparser/FMSXCM.xml | 36 ++++++++++++++++-------------------- xcmparser/Program.cs | 22 +++------------------- 4 files changed, 26 insertions(+), 39 deletions(-) diff --git a/libxcm/Message.cs b/libxcm/Message.cs index 2029c46..2efa65d 100644 --- a/libxcm/Message.cs +++ b/libxcm/Message.cs @@ -45,6 +45,10 @@ protected Message(XmlNode messageNode, List knownSymbols, Func knownSymbols, Func DocuText { get; set; } = new List(); public bool IdentifierIsString { get; protected set; } = false; diff --git a/xcm.xsd b/xcm.xsd index 4df4f82..c74d94c 100644 --- a/xcm.xsd +++ b/xcm.xsd @@ -44,6 +44,7 @@ + diff --git a/xcmparser/FMSXCM.xml b/xcmparser/FMSXCM.xml index b9d53b2..ecd36f5 100644 --- a/xcmparser/FMSXCM.xml +++ b/xcmparser/FMSXCM.xml @@ -21,7 +21,7 @@ - + @@ -193,54 +193,50 @@ - + - + - + - + - + - + m - - m/s - (height_diff/Header_Timestamp_diff)*1000 - - + - + - + @@ -248,7 +244,7 @@ - + @@ -256,20 +252,20 @@ - + - + - + @@ -279,7 +275,7 @@ - + @@ -298,7 +294,7 @@ - + diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 52c37ba..34f202f 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -331,32 +331,16 @@ static IEnumerable> GetCommandEntries(DataCo } } - static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, TelegrafUDPStream stream) - { - bool ret = false; - foreach (var msg in tokenizer.GetObjects()) - { - var matcher = msg.GetMatchFunction(); - if (matcher(data)) - { - msg.ParseMessage(data.Skip(msg.IDByteLength + msg.IDOffset)); - ret = true; - Console.WriteLine(TelegrafUDPStream.ConvertDataToJSON(msg)); - stream.SendData(msg); - } - } - return ret; - } - static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, DataStream stream, Options options = null) { bool ret = false; foreach (var msg in tokenizer.GetObjects()) { + var processData = data.Skip(msg.IDPrefixLength); var matcher = msg.GetMatchFunction(); - if (matcher(data)) + if (matcher(processData)) { - msg.ParseMessage(data.Skip(msg.IDByteLength + msg.IDOffset)); + msg.ParseMessage(processData.Skip(msg.IDByteLength + msg.IDOffset)); ret = true; if (options != null && options.Verbose) From d840b3a74f6f61e6f06f3cf970edd7b2e3982d24 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Fri, 25 Mar 2022 13:18:32 +0100 Subject: [PATCH 03/27] Add optional websocket connection --- xcmparser/Program.cs | 83 ++++++++++++++------ xcmparser/WebSocketStream.cs | 132 ++++++++++++++++++++++++++++++++ xcmparser/xcmparser.csproj | 1 + xcmparser/xcmparser.csproj.user | 2 +- 4 files changed, 195 insertions(+), 23 deletions(-) create mode 100644 xcmparser/WebSocketStream.cs diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 34f202f..3990d9f 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -34,6 +34,8 @@ class Options public bool NoForward { get; set; } = false; [Option("enableinterface", HelpText = "Enables a basic text interface that can be opened by pressing the t key in the console window")] public bool EnableInterace { get; set; } = false; + [Option("websocket", HelpText = "Enables the websocket interface on the specified IPEndpoint eg: 127.0.0.1:9001")] + public string websocketAddress { get; set; } = string.Empty; } class Program { @@ -47,6 +49,13 @@ static int Main(string[] args) string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string workingDir = Directory.GetCurrentDirectory(); int ret = 0; + + Console.CancelKeyPress += (obj, eventargs) => + { + eventargs.Cancel = true; + cts.Cancel(); + }; + try { Parser.Default.ParseArguments(args).WithParsed(o => { optionsValid = true; opts = o; }); @@ -76,39 +85,67 @@ static int Main(string[] args) { XCMParserTokenizer tokenizer = new XCMParserTokenizer(doc); //using TelegrafUDPStream stream = new TelegrafUDPStream(new IPEndPoint(IPAddress.Any, 3550), IPEndPoint.Parse(args[1])); - { + { pipe.StartService(); client.StartService(); Console.WriteLine("Upstream interface and downstream interface started"); var token = cts.Token; - while (!token.IsCancellationRequested) + WebSocketStream wsstream = null; + if(!string.IsNullOrEmpty(opts.websocketAddress)) { try { - var msg = await pipe.ReadMessageAsync(token); - lock (opts) - { - ProcessMessage(msg.Data, tokenizer, client, opts); - } + var endpoint = IPEndPoint.Parse(opts.websocketAddress); + wsstream = new WebSocketStream(endpoint); + Console.WriteLine($"Websocket server started on: {endpoint}"); } - catch (OperationCanceledException) + catch(Exception ex) { - + Console.WriteLine("Couldn't create websocket server"); + Console.WriteLine(ex.Message); + wsstream = null; } - catch (AggregateException ex) + } + try + { + while (!token.IsCancellationRequested) { - if (!(ex.InnerException is TimeoutException)) + try + { + var msg = await pipe.ReadMessageAsync(token); + string jsondata; + lock (opts) + { + ProcessMessage(msg.Data, tokenizer, client, out jsondata, opts); + } + if (!string.IsNullOrWhiteSpace(jsondata)) + { + wsstream?.Send(jsondata); + } + } + catch (OperationCanceledException) { - Console.WriteLine("Unkown exception:"); - Console.WriteLine(ex); - cts.Cancel(); - return false; + + } + catch (AggregateException ex) + { + if (!(ex.InnerException is TimeoutException)) + { + Console.WriteLine("Unkown exception:"); + Console.WriteLine(ex); + cts.Cancel(); + return false; + } } } + return true; + } + finally + { + wsstream?.Dispose(); } - return true; } }); XCMParserTokenizer tokenizer = new XCMParserTokenizer(doc); @@ -123,14 +160,14 @@ static int Main(string[] args) Console.WriteLine("Startup successfull"); while (!token.IsCancellationRequested) { - if (Console.KeyAvailable) + if (opts.EnableInterace && Console.KeyAvailable) { var key = Console.ReadKey(true).Key; switch (key) { case ConsoleKey.T: bool enableInterface = false; - lock(opts) + lock (opts) { enableInterface = opts.EnableInterace; } @@ -293,7 +330,7 @@ static void CommandEditStateMachine(XCMParserTokenizer tokenizer, DataStream str } } } - catch(OperationCanceledException) + catch (OperationCanceledException) { } @@ -322,18 +359,19 @@ static void ResetConsole() static IEnumerable> GetCommandEntries(DataCommand msg) { - foreach(DataSymbol symbol in msg) + foreach (DataSymbol symbol in msg) { - foreach(DataEntry entry in symbol) + foreach (DataEntry entry in symbol) { yield return new KeyValuePair(symbol, entry); } } } - static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, DataStream stream, Options options = null) + static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, DataStream stream, out string jsondata, Options options = null) { bool ret = false; + jsondata = string.Empty; foreach (var msg in tokenizer.GetObjects()) { var processData = data.Skip(msg.IDPrefixLength); @@ -351,6 +389,7 @@ static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, if (options == null || !options.NoForward) { var json = TelegrafUDPStream.ConvertDataToJSON(msg); + jsondata = json; stream.PublishUpstreamData(new libconnection.Message(Encoding.UTF8.GetBytes(json))); } } diff --git a/xcmparser/WebSocketStream.cs b/xcmparser/WebSocketStream.cs new file mode 100644 index 0000000..3fdc366 --- /dev/null +++ b/xcmparser/WebSocketStream.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using WebSocketSharp; +using WebSocketSharp.Server; + +namespace xcmparser +{ + internal class WSDataStream : WebSocketBehavior + { + private WebSocketStream ws = null; + protected override void OnOpen() + { + Console.WriteLine($"Websocket connection established: {ID}"); + } + + protected override void OnClose(CloseEventArgs e) + { + ws?.DisconnectWebSocket(this); + Console.WriteLine($"Websocket disconnected: {ID}"); + } + + protected override void OnError(ErrorEventArgs e) + { + Console.WriteLine($"Websocket error on: {ID}"); + Console.WriteLine($"Error Message: {e.Message}"); + } + + protected override void OnMessage(MessageEventArgs e) + { + } + public void ConnectTo(WebSocketStream ws) + { + this.ws = ws; + ws.ConnectWebSocket(this); + } + + public new void Send(string data) + { + base.Send(data); + } + } + internal class WebSocketStream : IDisposable + { + private readonly WebSocketServer ws; + private readonly List wsStreams = new (); + private readonly Mutex streamListMutex = new (); + public WebSocketStream(IPEndPoint endpoint) + { + ws = new (endpoint.Address, endpoint.Port); + ws.AddWebSocketService("/data", (s) => + { + s.ConnectTo(this); + }); + ws.Start(); + } + + public void ConnectWebSocket(WSDataStream ws) + { + streamListMutex.WaitOne(); + try + { + if (ws != null && !wsStreams.Contains(ws)) + { + wsStreams.Add(ws); + } + } + finally + { + streamListMutex.ReleaseMutex(); + } + } + + public void DisconnectWebSocket(WSDataStream ws) + { + streamListMutex.WaitOne(); + try + { + wsStreams.Remove(ws); + } + finally { streamListMutex.ReleaseMutex(); } + } + + public void Send(string data) + { + var removeList = new List(); + streamListMutex.WaitOne(); + try + { + foreach (var stream in wsStreams) + { + if (stream.State == WebSocketState.Open) + { + try + { + stream.Send(data); + } + catch (Exception ex) + { + Console.WriteLine($"Websocket sending failed: {ex.Message}"); + } + } + else if (stream.State == WebSocketState.Closed) + { + removeList.Add(stream); + } + } + foreach (var stream in removeList) + { + DisconnectWebSocket(stream); + } + } + finally + { + streamListMutex.ReleaseMutex(); + } + } + + public void Stop() + { + ws.Stop(); + } + public void Dispose() + { + Stop(); + } + } +} diff --git a/xcmparser/xcmparser.csproj b/xcmparser/xcmparser.csproj index 12ad4b2..2695405 100644 --- a/xcmparser/xcmparser.csproj +++ b/xcmparser/xcmparser.csproj @@ -13,6 +13,7 @@ + diff --git a/xcmparser/xcmparser.csproj.user b/xcmparser/xcmparser.csproj.user index d553b2e..5cb5117 100644 --- a/xcmparser/xcmparser.csproj.user +++ b/xcmparser/xcmparser.csproj.user @@ -1,7 +1,7 @@  - <_LastSelectedProfileId>D:\Projekte\xcode\xcmparser\Properties\PublishProfiles\FolderProfile.pubxml + <_LastSelectedProfileId>D:\Projekte\Spaceteam\Programme\The Hound\xcode\xcmparser\Properties\PublishProfiles\FolderProfile.pubxml From c966d9e7794c9d15c62ebef50680752af3ae1853 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Fri, 25 Mar 2022 13:30:17 +0100 Subject: [PATCH 04/27] Update gitignore --- .gitignore | 4 +++- libxcm/.gitignore | 6 +++++- libxcmparse/.gitignore | 6 +++++- xcmparser/.gitignore | 6 +++++- xcmparser/xcmparser.csproj.user | 11 ----------- xcodegen/.gitignore | 6 +++++- xcodegen/xcodegen.csproj.user | 11 ----------- 7 files changed, 23 insertions(+), 27 deletions(-) delete mode 100644 xcmparser/xcmparser.csproj.user delete mode 100644 xcodegen/xcodegen.csproj.user diff --git a/.gitignore b/.gitignore index 2a48e46..dbb5e11 100644 --- a/.gitignore +++ b/.gitignore @@ -365,4 +365,6 @@ FodyWeavers.xsd *.so -*.o \ No newline at end of file +*.o + +*.csproj.user \ No newline at end of file diff --git a/libxcm/.gitignore b/libxcm/.gitignore index 4c26367..dbb5e11 100644 --- a/libxcm/.gitignore +++ b/libxcm/.gitignore @@ -1,3 +1,5 @@ +*/Properties/ + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -363,4 +365,6 @@ FodyWeavers.xsd *.so -*.o \ No newline at end of file +*.o + +*.csproj.user \ No newline at end of file diff --git a/libxcmparse/.gitignore b/libxcmparse/.gitignore index 4c26367..dbb5e11 100644 --- a/libxcmparse/.gitignore +++ b/libxcmparse/.gitignore @@ -1,3 +1,5 @@ +*/Properties/ + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -363,4 +365,6 @@ FodyWeavers.xsd *.so -*.o \ No newline at end of file +*.o + +*.csproj.user \ No newline at end of file diff --git a/xcmparser/.gitignore b/xcmparser/.gitignore index 4c26367..dbb5e11 100644 --- a/xcmparser/.gitignore +++ b/xcmparser/.gitignore @@ -1,3 +1,5 @@ +*/Properties/ + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -363,4 +365,6 @@ FodyWeavers.xsd *.so -*.o \ No newline at end of file +*.o + +*.csproj.user \ No newline at end of file diff --git a/xcmparser/xcmparser.csproj.user b/xcmparser/xcmparser.csproj.user deleted file mode 100644 index 5cb5117..0000000 --- a/xcmparser/xcmparser.csproj.user +++ /dev/null @@ -1,11 +0,0 @@ - - - - <_LastSelectedProfileId>D:\Projekte\Spaceteam\Programme\The Hound\xcode\xcmparser\Properties\PublishProfiles\FolderProfile.pubxml - - - - Designer - - - \ No newline at end of file diff --git a/xcodegen/.gitignore b/xcodegen/.gitignore index 4c26367..dbb5e11 100644 --- a/xcodegen/.gitignore +++ b/xcodegen/.gitignore @@ -1,3 +1,5 @@ +*/Properties/ + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -363,4 +365,6 @@ FodyWeavers.xsd *.so -*.o \ No newline at end of file +*.o + +*.csproj.user \ No newline at end of file diff --git a/xcodegen/xcodegen.csproj.user b/xcodegen/xcodegen.csproj.user deleted file mode 100644 index 7a59c35..0000000 --- a/xcodegen/xcodegen.csproj.user +++ /dev/null @@ -1,11 +0,0 @@ - - - - <_LastSelectedProfileId>D:\Projekte\xcode\xcodegen\Properties\PublishProfiles\FolderProfile.pubxml - - - - Designer - - - \ No newline at end of file From 9d01abff0ae00e83322f3bb666d90522a61332ba Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Wed, 30 Mar 2022 10:36:26 +0200 Subject: [PATCH 05/27] Update libconnection --- libconnection | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libconnection b/libconnection index a1fdb4a..d057714 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit a1fdb4aff448b1c9a25998bb81ea13c9da32ee46 +Subproject commit d057714751800862a9cdf460e0dc6f1fd243d14a From b63770cb8683553a566fb2d31799d264e9ac6ac7 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Wed, 30 Mar 2022 14:00:06 +0200 Subject: [PATCH 06/27] Add mathematical data checks --- libxcm/Entry.cs | 53 --------------- libxcmparse/DataObjects/DataEntry.cs | 81 ++++++++++++++++++++++- libxcmparse/DataObjects/DataMessage.cs | 1 + libxcmparse/DataObjects/DataSymbol.cs | 1 + libxcmparse/libxcmparse.csproj | 1 - xcmparser/BigIntegerConverter.cs | 30 +++++++++ xcmparser/JsonConverter.cs | 91 ++++++++++++++++++++++++++ xcmparser/Program.cs | 25 ++++++- xcmparser/TelegrafUDPStream.cs | 78 ++-------------------- 9 files changed, 227 insertions(+), 134 deletions(-) create mode 100644 xcmparser/BigIntegerConverter.cs create mode 100644 xcmparser/JsonConverter.cs diff --git a/libxcm/Entry.cs b/libxcm/Entry.cs index 6ce66e1..9bcce56 100644 --- a/libxcm/Entry.cs +++ b/libxcm/Entry.cs @@ -53,19 +53,6 @@ public Entry(XmlNode entryNode) IsFixedCommand = entryNode.Attributes["fixed"].Value.ToLower() == "true"; } - if(entryNode.Attributes["importand"] != null) - { - IsImportand = entryNode.Attributes["importand"].Value.ToLower() == "true"; - } - - string checkString = null; - if (entryNode.Attributes["check"] != null) - { - checkString = entryNode.Attributes["check"].Value; - IsImportand = true; - IsChecked = true; - } - switch (type.ToLower()) { case "int": @@ -79,11 +66,6 @@ public Entry(XmlNode entryNode) { Value.SetValue(valueString); } - CheckValue = new FixedNumber(bitlength, true); - if (checkString != null) - { - CheckValue.SetValue(checkString); - } break; case "uint": BaseType = "fixed"; @@ -96,11 +78,6 @@ public Entry(XmlNode entryNode) { Value.SetValue(valueString); } - CheckValue = new FixedNumber(bitlength, false); - if (checkString != null) - { - CheckValue.SetValue(checkString); - } break; case "bool": BaseType = "bool"; @@ -123,11 +100,6 @@ public Entry(XmlNode entryNode) { Value.SetValue(valueString); } - CheckValue = new Bool(); - if (checkString != null) - { - CheckValue.SetValue(checkString); - } break; case "double": BaseType = "double"; @@ -136,11 +108,6 @@ public Entry(XmlNode entryNode) { Value.SetValue(valueString); } - CheckValue = new FloatingPointNumber(FloatingPointType.@double); - if (checkString != null) - { - CheckValue.SetValue(checkString); - } break; case "float": BaseType = "float"; @@ -149,11 +116,6 @@ public Entry(XmlNode entryNode) { Value.SetValue(valueString); } - CheckValue = new FloatingPointNumber(FloatingPointType.@float); - if (checkString != null) - { - CheckValue.SetValue(checkString); - } break; case "string": BaseType = "string"; @@ -237,21 +199,6 @@ public bool TryGetValue(ref T value) } } - public IType CheckValue { get; set; } - - public bool IsImportand { get; set; } = false; - - public bool IsChecked { get; set; } = false; - - public bool CheckValueForCorrectness() - { - if(IsChecked && Value != null && CheckValue != null) - { - return Value.Equals(CheckValue); - } - return true; - } - public int MaxBitLength { get diff --git a/libxcmparse/DataObjects/DataEntry.cs b/libxcmparse/DataObjects/DataEntry.cs index b307976..5fee864 100644 --- a/libxcmparse/DataObjects/DataEntry.cs +++ b/libxcmparse/DataObjects/DataEntry.cs @@ -24,6 +24,8 @@ public enum HistoryType private HistoryType historytype = HistoryType.roll; private IType[] type = null; private int historyindex = 0; + private Expression validExpr = null; + private Expression warningExpr = null; public DataEntry(XmlNode entryNode) : base(entryNode) { if(entryNode.Attributes["hashistory"] != null) @@ -190,6 +192,30 @@ public DataEntry(XmlNode entryNode) : base(entryNode) case "unit": Unit = childNode.FirstChild.Value; break; + case "valid": + try + { + validExpr = new Expression(childNode.FirstChild.Value); + validExpr.Parameters.Add("value", GetValue()); + IsValid = (bool)validExpr.Evaluate(); + } + catch(InvalidCastException) + { + throw new XmlException($"Error in Element {entryNode.OuterXml}\n Invalid ValidExpression in {Name} found. Expression must return a boolean"); + } + break; + case "warning": + try + { + warningExpr = new Expression(childNode.FirstChild.Value); + warningExpr.Parameters.Add("value", GetValue()); + HasWarning = (bool)warningExpr.Evaluate(); + } + catch (InvalidCastException) + { + throw new XmlException($"Error in Element {entryNode.OuterXml}\n Invalid WarningExpression in {Name} found. Expression must return a boolean"); + } + break; } } } @@ -224,11 +250,14 @@ private void RollArray(IType[] arr) public string Unit { get; private set; } = ""; + public bool IsValid { get; private set; } = true; + public bool HasWarning { get; private set; } = false; + public int SetValue(IEnumerable data) { - if(HasHistory) + if (HasHistory) { - if(historyindex >= type.Length) + if (historyindex >= type.Length) { switch (historytype) { @@ -246,10 +275,56 @@ public int SetValue(IEnumerable data) } int ret = type[historyindex].SetValue(data as byte[] ?? data.ToArray()); historyindex++; + IsValid = true; + HasWarning = false; return ret; } else - return Value.SetValue(data as byte[] ?? data.ToArray()); + { + int ret = Value.SetValue(data as byte[] ?? data.ToArray()); + + if(validExpr != null) + { + if (validExpr.Parameters.ContainsKey("value")) + { + validExpr.Parameters["value"] = GetValue(); + } + else + { + validExpr.Parameters.Add("value", GetValue()); + } + try + { + IsValid = (bool)validExpr.Evaluate(); + } + catch(InvalidCastException) + { + throw new XmlException($"Error in Element {(Parent.Parent as DataMessage)?.Name}:{Parent.Name}:{Name} Invalid ValidExpression. Expression must return a boolean"); + } + } + + if (warningExpr != null) + { + if (warningExpr.Parameters.ContainsKey("value")) + { + warningExpr.Parameters["value"] = GetValue(); + } + else + { + warningExpr.Parameters.Add("value", GetValue()); + } + try + { + HasWarning = (bool)warningExpr.Evaluate(); + } + catch(InvalidCastException) + { + throw new XmlException($"Error in Element {(Parent.Parent as DataMessage)?.Name}:{Parent.Name}:{Name} Invalid WarningExpression. Expression must return a boolean"); + } + } + + return ret; + } } public void SetValue(object data) diff --git a/libxcmparse/DataObjects/DataMessage.cs b/libxcmparse/DataObjects/DataMessage.cs index b85cb70..12ce4f4 100644 --- a/libxcmparse/DataObjects/DataMessage.cs +++ b/libxcmparse/DataObjects/DataMessage.cs @@ -32,6 +32,7 @@ public DataMessage(XmlNode messageNode, List knownSymbols) : base(messag } foreach (DataSymbol symb in this) { + symb.Parent = this; symb.Sibblings = sibblings; } } diff --git a/libxcmparse/DataObjects/DataSymbol.cs b/libxcmparse/DataObjects/DataSymbol.cs index dd3c48e..9bfe4a9 100644 --- a/libxcmparse/DataObjects/DataSymbol.cs +++ b/libxcmparse/DataObjects/DataSymbol.cs @@ -62,6 +62,7 @@ public byte[] GetData() } public List Sibblings { get; set; } = new List(); + public object Parent { get; set; } = null; public override Symbol Copy(bool fullCopy = false) { diff --git a/libxcmparse/libxcmparse.csproj b/libxcmparse/libxcmparse.csproj index d612f37..d176ec6 100644 --- a/libxcmparse/libxcmparse.csproj +++ b/libxcmparse/libxcmparse.csproj @@ -6,7 +6,6 @@ - diff --git a/xcmparser/BigIntegerConverter.cs b/xcmparser/BigIntegerConverter.cs new file mode 100644 index 0000000..98b5772 --- /dev/null +++ b/xcmparser/BigIntegerConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace xcmparser +{ + public class BigIntegerConverter : JsonConverter + { + public override BigInteger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.Number) + throw new JsonException(string.Format("Found token {0} but expected token {1}", reader.TokenType, JsonTokenType.Number)); + using var doc = JsonDocument.ParseValue(ref reader); + return BigInteger.Parse(doc.RootElement.GetRawText(), NumberFormatInfo.InvariantInfo); + } + + public override void Write(Utf8JsonWriter writer, BigInteger value, JsonSerializerOptions options) + { + var s = value.ToString(NumberFormatInfo.InvariantInfo); + using var doc = JsonDocument.Parse(s); + doc.WriteTo(writer); + } + } +} diff --git a/xcmparser/JsonConverter.cs b/xcmparser/JsonConverter.cs new file mode 100644 index 0000000..ccaa2c8 --- /dev/null +++ b/xcmparser/JsonConverter.cs @@ -0,0 +1,91 @@ +using libxcm; +using libxcmparse.DataObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace xcmparser +{ + static class JsonConverter + { + private static object SymbolToTagedObject(DataSymbol symb) + { + Dictionary tags = new Dictionary(); + foreach (DataEntry entry in symb) + { + var val = EntryToExtendedJSON(entry); + string name = entry.Name; + /* + switch (val) + { + case bool: + name += "_bool"; + break; + case string: + name += "_string"; + break; + default: + break; + }*/ + tags.Add(name, val); + } + return tags; + } + + public static byte[] ConvertDataToJSONByte(DataMessage msg) + { + return Encoding.UTF8.GetBytes(ConvertDataToJSON(msg)); + } + + public static object EntryToExtendedJSON(DataEntry entry) + { + return new + { + name = entry.Name, + value = entry.GetValue(), + isEntry = true, + isValid = entry.IsValid, + hasWarning = entry.HasWarning, + }; + } + public static string ConvertDataToJSON(DataMessage msg, bool pretty = false) + { + Dictionary tags = new Dictionary(); + foreach (DataSymbol symbol in msg) + { + string name = symbol.Name; + //int counter = 0; + if (string.IsNullOrWhiteSpace(name)) + { + /* + do + { + name = "Anonymous" + counter; + counter++; + }while(tags.ContainsKey(name));*/ + foreach (DataEntry entry in symbol) + { + tags.Add(entry.Name, EntryToExtendedJSON(entry)); + } + } + else + { + tags.Add(name, SymbolToTagedObject(symbol)); + } + } + JsonSerializerOptions options = new JsonSerializerOptions(); + options.Converters.Add(new BigIntegerConverter()); + options.WriteIndented = pretty; + return JsonSerializer.Serialize(new + { + Type = "receiveddata", + MessageName = msg.Name, + Fields = tags, + isExtended = true + }, options); + } + } +} diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 3990d9f..5eddb78 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -187,6 +187,18 @@ static int Main(string[] args) break; } } + if(receiveTask.IsCompleted) + { + if(receiveTask.Exception != null) + { + foreach(var e in receiveTask.Exception.InnerExceptions) + { + Console.WriteLine(e); + } + } + ret = -3; + break; + } Thread.Sleep(10); } } @@ -203,7 +215,14 @@ static int Main(string[] args) } cts?.Cancel(); Console.WriteLine("Shutting down"); - receiveTask?.Wait(1000); + try + { + receiveTask?.Wait(1000); + } + catch(Exception) + { + + } return ret; } @@ -383,12 +402,12 @@ static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, if (options != null && options.Verbose) { - var json = TelegrafUDPStream.ConvertDataToJSON(msg, true); + var json = JsonConverter.ConvertDataToJSON(msg, true); Console.WriteLine(json); } if (options == null || !options.NoForward) { - var json = TelegrafUDPStream.ConvertDataToJSON(msg); + var json = JsonConverter.ConvertDataToJSON(msg); jsondata = json; stream.PublishUpstreamData(new libconnection.Message(Encoding.UTF8.GetBytes(json))); } diff --git a/xcmparser/TelegrafUDPStream.cs b/xcmparser/TelegrafUDPStream.cs index 1107ecf..a1ae764 100644 --- a/xcmparser/TelegrafUDPStream.cs +++ b/xcmparser/TelegrafUDPStream.cs @@ -1,4 +1,5 @@ using libxcm; +using libxcmparse.DataObjects; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -16,23 +17,6 @@ namespace xcmparser { - public class BigIntegerConverter : JsonConverter - { - public override BigInteger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.Number) - throw new JsonException(string.Format("Found token {0} but expected token {1}", reader.TokenType, JsonTokenType.Number)); - using var doc = JsonDocument.ParseValue(ref reader); - return BigInteger.Parse(doc.RootElement.GetRawText(), NumberFormatInfo.InvariantInfo); - } - - public override void Write(Utf8JsonWriter writer, BigInteger value, JsonSerializerOptions options) - { - var s = value.ToString(NumberFormatInfo.InvariantInfo); - using var doc = JsonDocument.Parse(s); - doc.WriteTo(writer); - } - } internal class TelegrafUDPStream : IDisposable { @@ -103,67 +87,13 @@ public async Task ReadMessageAsync() return ret; } - private static object SymbolToTagedObject(Symbol symb) - { - Dictionary tags = new Dictionary(); - foreach(var entry in symb) - { - var val = entry.GetValue(); - string name = entry.Name; - switch (val) - { - case bool: - name += "_bool"; - break; - case string: - name += "_string"; - break; - default: - break; - } - tags.Add(name, val); - } - return tags; - } - - public static byte[] ConvertDataToJSONByte(Message msg) - { - return Encoding.UTF8.GetBytes(ConvertDataToJSON(msg)); - } - public static string ConvertDataToJSON(Message msg, bool pretty = false) - { - Dictionary tags = new Dictionary(); - foreach (var symbol in msg) - { - string name = symbol.Name; - int counter = 0; - if(string.IsNullOrWhiteSpace(name)) - { - do - { - name = "Anonymous" + counter; - counter++; - }while(tags.ContainsKey(name)); - } - tags.Add(name, SymbolToTagedObject(symbol)); - } - JsonSerializerOptions options = new JsonSerializerOptions(); - options.Converters.Add(new BigIntegerConverter()); - options.WriteIndented = pretty; - return JsonSerializer.Serialize(new - { - MessageName = msg.Name, - Fields = tags - },options); - } - - public void SendData(Message msg) + public void SendData(DataMessage msg) { - byte[] data = ConvertDataToJSONByte(msg); + byte[] data = JsonConverter.ConvertDataToJSONByte(msg); client.Send(data, data.Length); } - public async Task SendDataAsync(Message msg) + public async Task SendDataAsync(DataMessage msg) { await Task.Run(() => { From 56a0159c02e14a675d935751c72be8a4ac7edcd6 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Fri, 15 Apr 2022 00:25:09 +0200 Subject: [PATCH 07/27] Add field hasChecks to data Added hasChecks field to DataEntry, DataMessage and DataSymbol to show if a message, symbol or entry has any checks applied to it. If an entry has at least one check the containing symbol and message are also marked to hasChecks on them. --- libxcmparse/DataObjects/DataEntry.cs | 4 ++++ libxcmparse/DataObjects/DataMessage.cs | 24 ++++++++++++++++++++++++ libxcmparse/DataObjects/DataSymbol.cs | 23 +++++++++++++++++++++++ xcm.xsd | 5 ++++- xcmparser/JsonConverter.cs | 16 +++++++++++++--- 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/libxcmparse/DataObjects/DataEntry.cs b/libxcmparse/DataObjects/DataEntry.cs index 5fee864..a84fbdb 100644 --- a/libxcmparse/DataObjects/DataEntry.cs +++ b/libxcmparse/DataObjects/DataEntry.cs @@ -193,6 +193,7 @@ public DataEntry(XmlNode entryNode) : base(entryNode) Unit = childNode.FirstChild.Value; break; case "valid": + HasChecks = true; try { validExpr = new Expression(childNode.FirstChild.Value); @@ -205,6 +206,7 @@ public DataEntry(XmlNode entryNode) : base(entryNode) } break; case "warning": + HasChecks = true; try { warningExpr = new Expression(childNode.FirstChild.Value); @@ -253,6 +255,8 @@ private void RollArray(IType[] arr) public bool IsValid { get; private set; } = true; public bool HasWarning { get; private set; } = false; + public bool HasChecks { get; private set; } = false; + public int SetValue(IEnumerable data) { if (HasHistory) diff --git a/libxcmparse/DataObjects/DataMessage.cs b/libxcmparse/DataObjects/DataMessage.cs index 12ce4f4..f0a5f27 100644 --- a/libxcmparse/DataObjects/DataMessage.cs +++ b/libxcmparse/DataObjects/DataMessage.cs @@ -65,6 +65,30 @@ public void ParseMessage(IEnumerable message) Received?.Invoke(this, new EventArgs()); } + public bool CheckValidity() + { + bool ret = true; + foreach(DataSymbol symbol in this) + { + ret &= symbol.CheckValidity(); + } + return ret; + } + + public bool HasChecks + { + get + { + bool ret = false; + foreach (DataSymbol symbol in this) + { + ret |= symbol.HasChecks; + } + return ret; + } + } + + public byte[] GetData() { if (IsVariableLength) diff --git a/libxcmparse/DataObjects/DataSymbol.cs b/libxcmparse/DataObjects/DataSymbol.cs index 9bfe4a9..51e4369 100644 --- a/libxcmparse/DataObjects/DataSymbol.cs +++ b/libxcmparse/DataObjects/DataSymbol.cs @@ -61,6 +61,29 @@ public byte[] GetData() return ret; } + public bool CheckValidity() + { + bool ret = true; + foreach(DataEntry entry in this) + { + ret &= entry.IsValid; + } + return ret; + } + + public bool HasChecks + { + get + { + bool ret = false; + foreach (DataEntry entry in this) + { + ret |= entry.HasChecks; + } + return ret; + } + } + public List Sibblings { get; set; } = new List(); public object Parent { get; set; } = null; diff --git a/xcm.xsd b/xcm.xsd index c74d94c..04bc267 100644 --- a/xcm.xsd +++ b/xcm.xsd @@ -3,11 +3,14 @@ + + - + + diff --git a/xcmparser/JsonConverter.cs b/xcmparser/JsonConverter.cs index ccaa2c8..41d282a 100644 --- a/xcmparser/JsonConverter.cs +++ b/xcmparser/JsonConverter.cs @@ -32,7 +32,14 @@ private static object SymbolToTagedObject(DataSymbol symb) }*/ tags.Add(name, val); } - return tags; + return new + { + value = tags, + isValid = symb.CheckValidity(), + isEntry = false, + isSymbol = true, + hasChecks = symb.HasChecks + }; } public static byte[] ConvertDataToJSONByte(DataMessage msg) @@ -44,11 +51,12 @@ public static object EntryToExtendedJSON(DataEntry entry) { return new { - name = entry.Name, value = entry.GetValue(), isEntry = true, + isSymbol = false, isValid = entry.IsValid, hasWarning = entry.HasWarning, + hasChecks = entry.HasChecks, }; } public static string ConvertDataToJSON(DataMessage msg, bool pretty = false) @@ -84,7 +92,9 @@ public static string ConvertDataToJSON(DataMessage msg, bool pretty = false) Type = "receiveddata", MessageName = msg.Name, Fields = tags, - isExtended = true + isExtended = true, + isValid = msg.CheckValidity(), + hasChecks = msg.HasChecks, }, options); } } From 33df4cdd88553074ed15527ca62bf9f187db385c Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Mon, 13 Mar 2023 00:05:54 +0100 Subject: [PATCH 08/27] Change libconnection to the testing branch --- libconnection | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libconnection b/libconnection index d057714..c81b50c 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit d057714751800862a9cdf460e0dc6f1fd243d14a +Subproject commit c81b50ca184b3d9c891ae7cbca9aca13c2d28349 From 54a9a8c02c38afb7e2fc258663a55a3af53e0317 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Mon, 13 Mar 2023 00:08:28 +0100 Subject: [PATCH 09/27] Change xcm system Allow multiple systems per xcm file Add transmission queues to xcm file and allow queues to be applied to every system independently Removed Upstream/Downstream and changed to a simple Sending/Receiving scheme --- {xcmparser => libxcm}/BigIntegerConverter.cs | 2 +- libxcm/Command.cs | 4 +- libxcm/Connection.cs | 59 +++++++++++++ libxcm/Entry.cs | 4 + libxcm/IMessageConverter.cs | 13 +++ libxcm/ITokenizerFactory.cs | 13 +++ {xcmparser => libxcm}/JsonConverter.cs | 47 ++++------- libxcm/Message.cs | 13 ++- libxcm/MessageGroup.cs | 2 + libxcm/XCMDokument.cs | 79 +++++++++++++++++ libxcm/XCMFactory.cs | 22 +++++ libxcm/XCMTokenizer.cs | 85 +++++++++++++++---- libxcm/libxcm.csproj | 6 +- libxcmparse/DataObjects/DataCommand.cs | 8 +- libxcmparse/DataObjects/DataMessage.cs | 14 +-- libxcmparse/XCMParserTokenizer.cs | 21 +++-- libxcmparse/XCMParserTokenizerFactory.cs | 24 ++++++ libxcmparse/libxcmparse.csproj | 1 + xcmparser/Program.cs | 89 +++----------------- xcmparser/pruefstand.xml | 36 ++++++++ xcmparser/xcmparser.csproj | 3 + 21 files changed, 394 insertions(+), 151 deletions(-) rename {xcmparser => libxcm}/BigIntegerConverter.cs (98%) create mode 100644 libxcm/Connection.cs create mode 100644 libxcm/IMessageConverter.cs create mode 100644 libxcm/ITokenizerFactory.cs rename {xcmparser => libxcm}/JsonConverter.cs (60%) create mode 100644 libxcm/XCMDokument.cs create mode 100644 libxcm/XCMFactory.cs create mode 100644 libxcmparse/XCMParserTokenizerFactory.cs create mode 100644 xcmparser/pruefstand.xml diff --git a/xcmparser/BigIntegerConverter.cs b/libxcm/BigIntegerConverter.cs similarity index 98% rename from xcmparser/BigIntegerConverter.cs rename to libxcm/BigIntegerConverter.cs index 98b5772..0565bd9 100644 --- a/xcmparser/BigIntegerConverter.cs +++ b/libxcm/BigIntegerConverter.cs @@ -8,7 +8,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; -namespace xcmparser +namespace libxcm { public class BigIntegerConverter : JsonConverter { diff --git a/libxcm/Command.cs b/libxcm/Command.cs index 3c588c5..8e96cc5 100644 --- a/libxcm/Command.cs +++ b/libxcm/Command.cs @@ -7,11 +7,11 @@ namespace libxcm { public class Command : Message { - public Command(XmlNode commandNode, List knownSymbols) : base(commandNode, knownSymbols) + public Command(XmlNode commandNode, List knownSymbols, Connection inbound, Connection outbound) : base(commandNode, knownSymbols, inbound, outbound) { } - protected Command(XmlNode commandNode, List knownSymbols, Func symbolFactory, Func EntryFactory) : base(commandNode, knownSymbols, symbolFactory, EntryFactory) + protected Command(XmlNode commandNode, List knownSymbols, Func symbolFactory, Func EntryFactory, Connection inbound, Connection outbound) : base(commandNode, knownSymbols, symbolFactory, EntryFactory, inbound, outbound) { } } diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs new file mode 100644 index 0000000..24ab417 --- /dev/null +++ b/libxcm/Connection.cs @@ -0,0 +1,59 @@ +using libconnection; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; +using System.Xml; + +namespace libxcm +{ + public class Connection : StreamPipe + { + public Connection(XmlNode node) + { + foreach(XmlNode stage in node) + { + if(stage.NodeType != XmlNodeType.Comment) + { + if(stage.Name.ToLower() == "stage") + { + string className = ""; + List parameters = new List(); + foreach (XmlNode parameter in stage) + { + if (stage.Name.ToLower() == "stage") + { + switch(parameter.Name.ToLower()) + { + case "class": + className = parameter.InnerText; + break; + case "parameter": + parameters.Add(parameter.InnerText); + break; + } + } + } + if(className == "") + { + throw new ArgumentException("A connection must have a class element inside it"); + } + var datastream = NameResolver.GetStreamByName(className, parameters); + if (datastream == null) + throw new ArgumentException($"There is no connection class with the name {className}"); + + Add(datastream); + } + } + } + } + + public IMessageConverter MessageConverter { get; set; } = new xcmparser.JsonConverter(); + + public void TransmitParsedData(Message msg) + { + var data = MessageConverter.ConvertToByteArray(msg); + PublishDownstreamData(new libconnection.Message(data)); + } + } +} diff --git a/libxcm/Entry.cs b/libxcm/Entry.cs index 9bcce56..e7df7c0 100644 --- a/libxcm/Entry.cs +++ b/libxcm/Entry.cs @@ -20,10 +20,12 @@ public Entry(XmlNode entryNode) if (entryNode.Attributes["name"] == null) { Name = "AnonymousEntry"; + IsAnonymous = true; } else { Name = entryNode.Attributes["name"].Value; + IsAnonymous = false; } if (entryNode.Attributes["visibility"] != null && entryNode.Attributes["visibility"].Value == "virtual") @@ -174,6 +176,8 @@ public Entry(XmlNode entryNode) public bool IsVisible { get; protected set; } = true; + public bool IsAnonymous { get; protected set; } = false; + public IType Value { get; set; } public T GetValue() diff --git a/libxcm/IMessageConverter.cs b/libxcm/IMessageConverter.cs new file mode 100644 index 0000000..9031362 --- /dev/null +++ b/libxcm/IMessageConverter.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace libxcm +{ + public interface IMessageConverter + { + public byte[] ConvertToByteArray(Message msg); + } +} diff --git a/libxcm/ITokenizerFactory.cs b/libxcm/ITokenizerFactory.cs new file mode 100644 index 0000000..c65618f --- /dev/null +++ b/libxcm/ITokenizerFactory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; + +namespace libxcm +{ + public interface ITokenizerFactory + { + XCMTokenizer BuildTokenizer(XmlNode root, Dictionary inboundConnection, Dictionary outboundConnection); + XCMTokenizer BuildTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnection, Dictionary outboundConnection); + } +} diff --git a/xcmparser/JsonConverter.cs b/libxcm/JsonConverter.cs similarity index 60% rename from xcmparser/JsonConverter.cs rename to libxcm/JsonConverter.cs index 41d282a..6cc4f55 100644 --- a/xcmparser/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -1,5 +1,4 @@ using libxcm; -using libxcmparse.DataObjects; using System; using System.Collections.Generic; using System.Linq; @@ -9,60 +8,43 @@ namespace xcmparser { - static class JsonConverter + public class JsonConverter : IMessageConverter { - private static object SymbolToTagedObject(DataSymbol symb) + private static object SymbolToTagedObject(Symbol symb) { Dictionary tags = new Dictionary(); - foreach (DataEntry entry in symb) + foreach (Entry entry in symb) { var val = EntryToExtendedJSON(entry); string name = entry.Name; - /* - switch (val) - { - case bool: - name += "_bool"; - break; - case string: - name += "_string"; - break; - default: - break; - }*/ tags.Add(name, val); } return new { value = tags, - isValid = symb.CheckValidity(), isEntry = false, - isSymbol = true, - hasChecks = symb.HasChecks + isSymbol = true }; } - public static byte[] ConvertDataToJSONByte(DataMessage msg) + public static byte[] ConvertDataToJSONByte(Message msg) { return Encoding.UTF8.GetBytes(ConvertDataToJSON(msg)); } - public static object EntryToExtendedJSON(DataEntry entry) + public static object EntryToExtendedJSON(Entry entry) { return new { value = entry.GetValue(), isEntry = true, - isSymbol = false, - isValid = entry.IsValid, - hasWarning = entry.HasWarning, - hasChecks = entry.HasChecks, + isSymbol = false }; } - public static string ConvertDataToJSON(DataMessage msg, bool pretty = false) + public static string ConvertDataToJSON(Message msg, bool pretty = false) { Dictionary tags = new Dictionary(); - foreach (DataSymbol symbol in msg) + foreach (Symbol symbol in msg) { string name = symbol.Name; //int counter = 0; @@ -74,7 +56,7 @@ public static string ConvertDataToJSON(DataMessage msg, bool pretty = false) name = "Anonymous" + counter; counter++; }while(tags.ContainsKey(name));*/ - foreach (DataEntry entry in symbol) + foreach (Entry entry in symbol) { tags.Add(entry.Name, EntryToExtendedJSON(entry)); } @@ -92,10 +74,13 @@ public static string ConvertDataToJSON(DataMessage msg, bool pretty = false) Type = "receiveddata", MessageName = msg.Name, Fields = tags, - isExtended = true, - isValid = msg.CheckValidity(), - hasChecks = msg.HasChecks, + isExtended = true }, options); } + + public byte[] ConvertToByteArray(Message msg) + { + return ConvertDataToJSONByte(msg); + } } } diff --git a/libxcm/Message.cs b/libxcm/Message.cs index 2efa65d..e0196e9 100644 --- a/libxcm/Message.cs +++ b/libxcm/Message.cs @@ -12,6 +12,8 @@ public class Message : IEnumerable { protected readonly byte[] identifier; protected readonly List symbols = new List(); + protected readonly Connection inbound; + protected readonly Connection outbound; public int IDOffset { get; protected set; } = 0; private int idlength = -1; @@ -26,13 +28,16 @@ private static Entry EntryFactory(XmlNode entryNode) return new Entry(entryNode); } - public Message(XmlNode messageNode, List knownSymbols) : this(messageNode, knownSymbols, SymbolFactory, EntryFactory) + public Message(XmlNode messageNode, List knownSymbols, Connection inbound, Connection outbound) : this(messageNode, knownSymbols, SymbolFactory, EntryFactory, inbound, outbound) { } - protected Message(XmlNode messageNode, List knownSymbols, Func symbolFactory, Func EntryFactory) + protected Message(XmlNode messageNode, List knownSymbols, Func symbolFactory, Func EntryFactory, Connection inbound, Connection outbound) { + this.inbound = inbound; + this.outbound = outbound; + bool isString; identifier = GetIdentifierData(messageNode, out isString); IdentifierIsString = isString; @@ -181,6 +186,7 @@ public Func,bool> GetMatchFunction() public virtual bool Match(IEnumerable data) { + /* bool groupmatched = true; if(Group != null) { @@ -189,7 +195,8 @@ public virtual bool Match(IEnumerable data) } groupmatched = groupmatched && data.Take(identifier.Length).SequenceEqual(identifier); - return groupmatched; + return groupmatched;*/ + return data.Take(identifier.Length).SequenceEqual(identifier); } public int GetBitlength() diff --git a/libxcm/MessageGroup.cs b/libxcm/MessageGroup.cs index 2d67435..7704ff7 100644 --- a/libxcm/MessageGroup.cs +++ b/libxcm/MessageGroup.cs @@ -8,6 +8,7 @@ namespace libxcm { public class MessageGroup { + /* protected readonly byte[] identifier; public MessageGroup(XmlNode groupNode, List knownSymbols, XCMTokenizer tokenizer) { @@ -54,5 +55,6 @@ public bool DataMatch(IEnumerable data) } public MessageGroup Group = null; + */ } } diff --git a/libxcm/XCMDokument.cs b/libxcm/XCMDokument.cs new file mode 100644 index 0000000..b800d9f --- /dev/null +++ b/libxcm/XCMDokument.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml; + +namespace libxcm +{ + public class XCMDokument + { + private List symbols = new List(); + private Dictionary tokenizers = new Dictionary(); + + private Dictionary incommingConnections = new Dictionary(); + private Dictionary outgoingConnections = new Dictionary(); + public XCMDokument(XmlDocument doc, ITokenizerFactory factory) : this(doc.DocumentElement, factory) + { + + } + + public XCMDokument(XmlElement root, ITokenizerFactory factory) + { + foreach (XmlElement incon in root.SelectNodes("/spacesystem/connections/incomming/connection")) //Read all incomming connections and generate connection elements from it + { + string connName = incon.GetAttribute("name"); + if(string.IsNullOrWhiteSpace(connName)) + { + throw new ArgumentException("Each connection element must have a name"); + } + if(incommingConnections.ContainsKey(connName)) + { + throw new ArgumentException("Each connection element must have a unique name"); + } + var connection = new Connection(incon); + connection.StartService(); + incommingConnections.Add(connName, connection); + } + + foreach (XmlElement outcon in root.SelectNodes("/spacesystem/connections/outgoing/connection")) //Read all outgoing connections and generate connection elements from it + { + string connName = outcon.GetAttribute("name"); + if (string.IsNullOrWhiteSpace(connName)) + { + throw new ArgumentException("Each connection element must have a name"); + } + if (outgoingConnections.ContainsKey(connName)) + { + throw new ArgumentException("Each connection element must have a unique name"); + } + var connection = new Connection(outcon); + connection.StartService(); + outgoingConnections.Add(connName, connection); + } + + foreach(XmlElement globalSymbol in root.SelectNodes("/spacesystem/symbols/symbol")) + { + + } + + foreach (XmlElement systemNode in root.SelectNodes("/spacesystem/system")) + { + string systemname = systemNode.Attributes["name"].Value; + if (systemname == null || tokenizers.ContainsKey(systemname)) + throw new ArgumentException("A system must have a unique name"); + var tokenizer = factory.BuildTokenizer(systemNode, symbols, incommingConnections, outgoingConnections); + tokenizers.Add(systemname, tokenizer); + } + } + + public IEnumerable GetTokenizers() + { + return tokenizers.Values; + } + + public XCMTokenizer GetTokenzierByName(string name) + { + return tokenizers[name]; + } + } +} diff --git a/libxcm/XCMFactory.cs b/libxcm/XCMFactory.cs new file mode 100644 index 0000000..3c3333d --- /dev/null +++ b/libxcm/XCMFactory.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace libxcm +{ + public class XCMFactory : ITokenizerFactory + { + public virtual XCMTokenizer BuildTokenizer(XmlNode root, Dictionary inboundConnection, Dictionary outboundConnection) + { + return BuildTokenizer(root, null, inboundConnection, outboundConnection); + } + + public XCMTokenizer BuildTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnection, Dictionary outboundConnection) + { + return new XCMTokenizer(root, symbols, inboundConnection, outboundConnection); + } + } +} diff --git a/libxcm/XCMTokenizer.cs b/libxcm/XCMTokenizer.cs index 11d5adb..6ebad16 100644 --- a/libxcm/XCMTokenizer.cs +++ b/libxcm/XCMTokenizer.cs @@ -3,6 +3,7 @@ using System.Text; using System.Xml; using System.Linq; +using System.Xml.Linq; namespace libxcm { @@ -10,24 +11,64 @@ public class XCMTokenizer { protected Dictionary> compiledtokens = new Dictionary>(); protected List knownSymbols = new List(); - public XCMTokenizer(XmlDocument doc) : this(doc.DocumentElement, new XmlNamespaceManager(doc.NameTable)) - { + protected Connection inbound; + protected Connection outbound; - } - public XCMTokenizer(XmlElement root, XmlNamespaceManager mngr) + public XCMTokenizer(XmlNode root, IEnumerable symbols, Connection inboundConnection, Connection outboundConnection) { - mngr.AddNamespace("sps", "http://www.w3schools.com/"); - foreach(XmlNode token in root) + if (inboundConnection == null || outboundConnection == null) + { + throw new ArgumentException("Each system must have a default input and output connection"); + } + + inbound = inboundConnection; + outbound = outboundConnection; + + if (symbols != null) + { + AddKownSymbols(symbols); + } + foreach (XmlNode token in root) { - foreach(XmlNode t in token) + if (token.NodeType != XmlNodeType.Comment) { - if(t.NodeType != XmlNodeType.Comment) + foreach (XmlNode t in token) { - BuildToken(token, t); + if (t.NodeType != XmlNodeType.Comment) + { + BuildToken(token, t); + } } } } } + public XCMTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnections, Dictionary outboundConnections) : this(root, symbols, GetInboundConnectionFromXML(root, inboundConnections), GetOutboundConnectionFromXML(root, outboundConnections)) + { + } + + private static Connection GetInboundConnectionFromXML(XmlNode node, Dictionary connections) + { + var defInput = node.SelectSingleNode("defaultInput"); + if(defInput == null) return null; + var connName = defInput.InnerText; + if(connections.ContainsKey(connName)) return connections[connName]; + else + { + return null; + } + } + + private static Connection GetOutboundConnectionFromXML(XmlNode node, Dictionary connections) + { + var defInput = node.SelectSingleNode("defaultOutput"); + if (defInput == null) return null; + var connName = defInput.InnerText; + if (connections.ContainsKey(connName)) return connections[connName]; + else + { + return null; + } + } public IEnumerable GetObjects(bool strict = true) { @@ -53,14 +94,22 @@ public IEnumerable GetObjects(bool strict = true) } } - virtual public Message BuildMessage(XmlNode node) + virtual protected void AddKownSymbols(IEnumerable symbols) { - return new Message(node, knownSymbols); + foreach(var symb in symbols) + { + knownSymbols.Add(symb.Copy()); + } } - virtual public Command BuildCommand(XmlNode node) + virtual public Message BuildMessage(XmlNode node, Connection inbound, Connection outbound) { - return new Command(node, knownSymbols); + return new Message(node, knownSymbols, inbound, outbound); + } + + virtual public Command BuildCommand(XmlNode node, Connection inbound, Connection outbound) + { + return new Command(node, knownSymbols, inbound, outbound); } virtual public Symbol BuildSymbol(XmlNode node) @@ -70,7 +119,8 @@ virtual public Symbol BuildSymbol(XmlNode node) virtual public MessageGroup BuildGroup(XmlNode node) { - return new MessageGroup(node, knownSymbols, this); + throw new NotImplementedException(); + //return new MessageGroup(node, knownSymbols, this); } virtual protected void BuildToken(XmlNode parent, XmlNode node) { @@ -90,22 +140,23 @@ virtual protected void BuildToken(XmlNode parent, XmlNode node) { compiledtokens.Add("message", new List()); } - compiledtokens["message"].Add(BuildMessage(node)); + compiledtokens["message"].Add(BuildMessage(node, inbound, outbound)); break; case "command": if (!compiledtokens.ContainsKey("command")) { compiledtokens.Add("command", new List()); } - compiledtokens["command"].Add(BuildCommand(node)); + compiledtokens["command"].Add(BuildCommand(node, inbound, outbound)); break; + /* case "Group": MessageGroup group = BuildGroup(node); foreach(Message msg in group.Messages) { compiledtokens["message"].Add(msg); } - break; + break;*/ default: break; } diff --git a/libxcm/libxcm.csproj b/libxcm/libxcm.csproj index dbdcea4..842a3af 100644 --- a/libxcm/libxcm.csproj +++ b/libxcm/libxcm.csproj @@ -1,7 +1,11 @@  - netstandard2.0 + net5.0 + + + + diff --git a/libxcmparse/DataObjects/DataCommand.cs b/libxcmparse/DataObjects/DataCommand.cs index 3e014e9..6702293 100644 --- a/libxcmparse/DataObjects/DataCommand.cs +++ b/libxcmparse/DataObjects/DataCommand.cs @@ -8,15 +8,11 @@ namespace libxcmparse.DataObjects { public class DataCommand : Command { - public static Command CommandFactory(XmlNode messageNode, List knownSymbols) - { - return new DataCommand(messageNode, knownSymbols); - } - public DataCommand(XmlNode commandNode, List knownSymbols) : this(commandNode, knownSymbols, DataMessage.SymbolFactory, DataMessage.EntryFactory) + public DataCommand(XmlNode commandNode, List knownSymbols, Connection inbound, Connection outbound) : this(commandNode, knownSymbols, DataMessage.SymbolFactory, DataMessage.EntryFactory, inbound, outbound) { } - protected DataCommand(XmlNode commandNode, List knownSymbols, Func symbolFactory, Func EntryFactory) : base(commandNode, knownSymbols, symbolFactory, EntryFactory) + protected DataCommand(XmlNode commandNode, List knownSymbols, Func symbolFactory, Func EntryFactory, Connection inbound, Connection outbound) : base(commandNode, knownSymbols, symbolFactory, EntryFactory, inbound, outbound) { List sibblings = new List(); foreach (DataSymbol symb in this) diff --git a/libxcmparse/DataObjects/DataMessage.cs b/libxcmparse/DataObjects/DataMessage.cs index f0a5f27..7f3134b 100644 --- a/libxcmparse/DataObjects/DataMessage.cs +++ b/libxcmparse/DataObjects/DataMessage.cs @@ -10,10 +10,6 @@ namespace libxcmparse.DataObjects public class DataMessage : Message, IEnumerable { public event EventHandler Received; - public static Message MessageFactory(XmlNode messageNode, List knownSymbols) - { - return new DataMessage(messageNode, knownSymbols); - } public static Symbol SymbolFactory(XmlNode symbolNode, bool isNested) { return new DataSymbol(symbolNode, isNested); @@ -23,7 +19,7 @@ public static Entry EntryFactory(XmlNode entryNode) { return new DataEntry(entryNode); } - public DataMessage(XmlNode messageNode, List knownSymbols) : base(messageNode, knownSymbols, SymbolFactory, EntryFactory) + public DataMessage(XmlNode messageNode, List knownSymbols, Connection inboundConnection, Connection outboundConnection) : base(messageNode, knownSymbols, SymbolFactory, EntryFactory, inboundConnection, outboundConnection) { List sibblings = new List(); foreach (DataSymbol symb in this) @@ -35,6 +31,13 @@ public DataMessage(XmlNode messageNode, List knownSymbols) : base(messag symb.Parent = this; symb.Sibblings = sibblings; } + + inbound.MessageReceived += Inbound_MessageReceived; + } + + private void Inbound_MessageReceived(object sender, libconnection.MessageEventArgs e) + { + ParseMessage(e.Message); } public void ParseMessage(IEnumerable message) @@ -63,6 +66,7 @@ public void ParseMessage(IEnumerable message) } } Received?.Invoke(this, new EventArgs()); + outbound.TransmitParsedData(this); } public bool CheckValidity() diff --git a/libxcmparse/XCMParserTokenizer.cs b/libxcmparse/XCMParserTokenizer.cs index 2ee1b0a..8aad73d 100644 --- a/libxcmparse/XCMParserTokenizer.cs +++ b/libxcmparse/XCMParserTokenizer.cs @@ -9,12 +9,15 @@ namespace xcmparser { public class XCMParserTokenizer : XCMTokenizer { - public XCMParserTokenizer(XmlDocument doc) : base(doc) - { - } - - public XCMParserTokenizer(XmlElement root, XmlNamespaceManager mngr) : base(root, mngr) + public XCMParserTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnection, Dictionary outboundConnection) : base(root, symbols, inboundConnection, outboundConnection) { + foreach(var s in symbols) + { + if(s is not DataSymbol) + { + throw new ArgumentException("The parser tokenizer only supports datasymbols"); + } + } } public override Symbol BuildSymbol(XmlNode node) @@ -22,14 +25,14 @@ public override Symbol BuildSymbol(XmlNode node) return new DataSymbol(node); } - public override Command BuildCommand(XmlNode node) + public override Command BuildCommand(XmlNode node, Connection inbound, Connection outbound) { - return new DataCommand(node, knownSymbols); + return new DataCommand(node, knownSymbols, inbound, outbound); } - public override Message BuildMessage(XmlNode node) + public override Message BuildMessage(XmlNode node, Connection inbound, Connection outbound) { - return new DataMessage(node, knownSymbols); + return new DataMessage(node, knownSymbols, inbound, outbound); } } } diff --git a/libxcmparse/XCMParserTokenizerFactory.cs b/libxcmparse/XCMParserTokenizerFactory.cs new file mode 100644 index 0000000..05b83b4 --- /dev/null +++ b/libxcmparse/XCMParserTokenizerFactory.cs @@ -0,0 +1,24 @@ +using libxcm; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using xcmparser; + +namespace libxcmparse +{ + public class XCMParserTokenizerFactory : ITokenizerFactory + { + public XCMTokenizer BuildTokenizer(XmlNode root, Dictionary inboundConnection, Dictionary outboundConnection) + { + return BuildTokenizer(root, null, inboundConnection, outboundConnection); + } + + public XCMTokenizer BuildTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnection, Dictionary outboundConnection) + { + return new XCMParserTokenizer(root, symbols, inboundConnection, outboundConnection); + } + } +} diff --git a/libxcmparse/libxcmparse.csproj b/libxcmparse/libxcmparse.csproj index d176ec6..6d63113 100644 --- a/libxcmparse/libxcmparse.csproj +++ b/libxcmparse/libxcmparse.csproj @@ -9,6 +9,7 @@ + diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 5eddb78..5e8e1dd 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -23,7 +23,7 @@ class Options { [Option('f', "xcmfile", Required = true, HelpText = "The xcm file with the package definitions")] public string Xcmfile { get; set; } - [Option('p', "pipe", Required = true, HelpText = "The pipe expression on how to receive the data")] + [Option('p', "pipe", HelpText = "The pipe expression on how to receive the data")] public string Pipe { get; set; } [Option('o', "outputinterface", HelpText = "Interface definition for the output UDP Server eg: 127.0.0.1:9000", Default = "127.0.0.1:9000")] public string OutputInterface { get; set; } @@ -41,7 +41,7 @@ class Program { static int Main(string[] args) { - using CancellationTokenSource cts = new CancellationTokenSource(); + using CancellationTokenSource cts = new(); Options opts = null; bool optionsValid = false; Task receiveTask = null; @@ -66,7 +66,7 @@ static int Main(string[] args) settings.Schemas.Add("http://www.w3schools.com/", Path.Combine(assemblyDir, "xcm.xsd")); settings.ValidationType = ValidationType.Schema; - XmlReader reader = XmlReader.Create(opts.Xcmfile, settings); + XmlReader reader = XmlReader.Create(opts.Xcmfile); XmlDocument doc = new XmlDocument(); try { @@ -78,79 +78,16 @@ static int Main(string[] args) Console.WriteLine(ex.Message); return -1; } - using var client = new UdpTransmitter(null, IPEndPoint.Parse(opts.OutputInterface)); - using var pipe = new StreamPipe(opts.Pipe); - Console.WriteLine("Starting interfaces"); - receiveTask = Task.Run(async () => - { - XCMParserTokenizer tokenizer = new XCMParserTokenizer(doc); - //using TelegrafUDPStream stream = new TelegrafUDPStream(new IPEndPoint(IPAddress.Any, 3550), IPEndPoint.Parse(args[1])); - { - pipe.StartService(); - client.StartService(); - Console.WriteLine("Upstream interface and downstream interface started"); + var tokenizerFactory = new XCMParserTokenizerFactory(); + var xcmdoc = new XCMDokument(doc, tokenizerFactory); - var token = cts.Token; - WebSocketStream wsstream = null; - if(!string.IsNullOrEmpty(opts.websocketAddress)) - { - try - { - var endpoint = IPEndPoint.Parse(opts.websocketAddress); - wsstream = new WebSocketStream(endpoint); - Console.WriteLine($"Websocket server started on: {endpoint}"); - } - catch(Exception ex) - { - Console.WriteLine("Couldn't create websocket server"); - Console.WriteLine(ex.Message); - wsstream = null; - } - } - try - { - while (!token.IsCancellationRequested) - { - try - { - var msg = await pipe.ReadMessageAsync(token); - string jsondata; - lock (opts) - { - ProcessMessage(msg.Data, tokenizer, client, out jsondata, opts); - } - if (!string.IsNullOrWhiteSpace(jsondata)) - { - wsstream?.Send(jsondata); - } - } - catch (OperationCanceledException) - { + while(true) + { - } - catch (AggregateException ex) - { - if (!(ex.InnerException is TimeoutException)) - { - Console.WriteLine("Unkown exception:"); - Console.WriteLine(ex); - cts.Cancel(); - return false; - } - } - } - return true; - } - finally - { - wsstream?.Dispose(); - } - } - }); - XCMParserTokenizer tokenizer = new XCMParserTokenizer(doc); - var token = cts.Token; - bool verbosity = opts.Verbose; + } + + /* Console.CancelKeyPress += (_, args) => { args.Cancel = true; @@ -200,7 +137,7 @@ static int Main(string[] args) break; } Thread.Sleep(10); - } + }*/ } else { @@ -378,9 +315,9 @@ static void ResetConsole() static IEnumerable> GetCommandEntries(DataCommand msg) { - foreach (DataSymbol symbol in msg) + foreach (DataSymbol symbol in msg.Cast()) { - foreach (DataEntry entry in symbol) + foreach (DataEntry entry in symbol.Cast()) { yield return new KeyValuePair(symbol, entry); } diff --git a/xcmparser/pruefstand.xml b/xcmparser/pruefstand.xml new file mode 100644 index 0000000..544b44f --- /dev/null +++ b/xcmparser/pruefstand.xml @@ -0,0 +1,36 @@ + + + + + + + serial + COM6 + + + smp + + + + + + + console + + + + + + + + testboardSerial + telegrafStream + + + + + + + + + \ No newline at end of file diff --git a/xcmparser/xcmparser.csproj b/xcmparser/xcmparser.csproj index 2695405..c5d2ef1 100644 --- a/xcmparser/xcmparser.csproj +++ b/xcmparser/xcmparser.csproj @@ -23,6 +23,9 @@ + + Always + Always From ac2a3497b2283b9d59b7b21ac0ac4ad7015db907 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Mon, 27 Mar 2023 08:48:22 +0200 Subject: [PATCH 10/27] Implement new libconnection --- libconnection | 2 +- libxcm/Connection.cs | 23 +++++++++++++++++++---- libxcm/JsonConverter.cs | 11 +---------- libxcm/XCMDokument.cs | 2 -- libxcmparse/DataObjects/DataMessage.cs | 2 +- xcmparser/Program.cs | 4 ++-- xcmparser/pruefstand.xml | 3 ++- 7 files changed, 26 insertions(+), 21 deletions(-) diff --git a/libconnection b/libconnection index c81b50c..a75b068 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit c81b50ca184b3d9c891ae7cbca9aca13c2d28349 +Subproject commit a75b0685ba4ce86d9e530ee8e7a4f21411ea4539 diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs index 24ab417..09cb6fc 100644 --- a/libxcm/Connection.cs +++ b/libxcm/Connection.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Text; using System.Text.Json.Serialization; +using System.Threading; using System.Xml; namespace libxcm { public class Connection : StreamPipe { + private object _lock = new object(); public Connection(XmlNode node) { foreach(XmlNode stage in node) @@ -18,7 +20,7 @@ public Connection(XmlNode node) if(stage.Name.ToLower() == "stage") { string className = ""; - List parameters = new List(); + Dictionary parameters = new (); foreach (XmlNode parameter in stage) { if (stage.Name.ToLower() == "stage") @@ -29,7 +31,12 @@ public Connection(XmlNode node) className = parameter.InnerText; break; case "parameter": - parameters.Add(parameter.InnerText); + string parameterName = parameter.Attributes["name"]?.Value; + if(string.IsNullOrWhiteSpace( parameterName ) ) + { + throw new ArgumentException("The connection class parameter must have a name attribute."); + } + parameters.Add(parameterName, parameter.InnerText); break; } } @@ -50,10 +57,18 @@ public Connection(XmlNode node) public IMessageConverter MessageConverter { get; set; } = new xcmparser.JsonConverter(); - public void TransmitParsedData(Message msg) + public void TransmitData(Message msg) { var data = MessageConverter.ConvertToByteArray(msg); - PublishDownstreamData(new libconnection.Message(data)); + TransmitMessage(new libconnection.Message(data)); + } + + public void TransmitDataSynchronized(Message msg) + { + lock(_lock) + { + TransmitData(msg); + } } } } diff --git a/libxcm/JsonConverter.cs b/libxcm/JsonConverter.cs index 6cc4f55..ef9d0a9 100644 --- a/libxcm/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -36,9 +36,7 @@ public static object EntryToExtendedJSON(Entry entry) { return new { - value = entry.GetValue(), - isEntry = true, - isSymbol = false + value = entry.GetValue() }; } public static string ConvertDataToJSON(Message msg, bool pretty = false) @@ -47,15 +45,8 @@ public static string ConvertDataToJSON(Message msg, bool pretty = false) foreach (Symbol symbol in msg) { string name = symbol.Name; - //int counter = 0; if (string.IsNullOrWhiteSpace(name)) { - /* - do - { - name = "Anonymous" + counter; - counter++; - }while(tags.ContainsKey(name));*/ foreach (Entry entry in symbol) { tags.Add(entry.Name, EntryToExtendedJSON(entry)); diff --git a/libxcm/XCMDokument.cs b/libxcm/XCMDokument.cs index b800d9f..9997786 100644 --- a/libxcm/XCMDokument.cs +++ b/libxcm/XCMDokument.cs @@ -31,7 +31,6 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory) throw new ArgumentException("Each connection element must have a unique name"); } var connection = new Connection(incon); - connection.StartService(); incommingConnections.Add(connName, connection); } @@ -47,7 +46,6 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory) throw new ArgumentException("Each connection element must have a unique name"); } var connection = new Connection(outcon); - connection.StartService(); outgoingConnections.Add(connName, connection); } diff --git a/libxcmparse/DataObjects/DataMessage.cs b/libxcmparse/DataObjects/DataMessage.cs index 7f3134b..bfefd72 100644 --- a/libxcmparse/DataObjects/DataMessage.cs +++ b/libxcmparse/DataObjects/DataMessage.cs @@ -66,7 +66,7 @@ public void ParseMessage(IEnumerable message) } } Received?.Invoke(this, new EventArgs()); - outbound.TransmitParsedData(this); + outbound.TransmitData(this); } public bool CheckValidity() diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 5e8e1dd..81de88c 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -238,7 +238,7 @@ static void CommandEditStateMachine(XCMParserTokenizer tokenizer, DataStream str //Send the command and return state = State.EditCommand; var data = selectedCommand.GetData(); - stream.PublishDownstreamData(new libconnection.Message(data)); + stream.TransmitMessage(new libconnection.Message(data)); } else { @@ -346,7 +346,7 @@ static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, { var json = JsonConverter.ConvertDataToJSON(msg); jsondata = json; - stream.PublishUpstreamData(new libconnection.Message(Encoding.UTF8.GetBytes(json))); + stream.TransmitMessage(new libconnection.Message(Encoding.UTF8.GetBytes(json))); } } } diff --git a/xcmparser/pruefstand.xml b/xcmparser/pruefstand.xml index 544b44f..4c3bb1c 100644 --- a/xcmparser/pruefstand.xml +++ b/xcmparser/pruefstand.xml @@ -15,7 +15,8 @@ - console + udp + 127.0.0.1:8000 From 4c7535210b5e9bbba6e387136ba62e7ef18fafa8 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Mon, 24 Apr 2023 09:39:12 +0200 Subject: [PATCH 11/27] Update libconnection --- libconnection | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libconnection b/libconnection index a75b068..07a840e 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit a75b0685ba4ce86d9e530ee8e7a4f21411ea4539 +Subproject commit 07a840e3696268f4115d7021476f7ae8d935d8cf From 57e8bf598a1c98d02c6d50280163e31ff4893829 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Mon, 24 Apr 2023 09:39:33 +0200 Subject: [PATCH 12/27] Add command parsing and transmission --- libxcm/Connection.cs | 13 +++++--- libxcm/JsonConverter.cs | 20 +++++++++++- libxcm/JsonTypes/JsonCommand.cs | 42 ++++++++++++++++++++++++++ libxcm/JsonTypes/JsonElement.cs | 19 ++++++++++++ libxcm/JsonTypes/JsonSymbol.cs | 24 +++++++++++++++ libxcm/XCMDokument.cs | 2 +- libxcm/libxcm.csproj | 2 +- libxcmparse/DataObjects/DataCommand.cs | 10 ++++++ libxcmparse/DataObjects/DataMessage.cs | 2 +- libxcmparse/libxcmparse.csproj | 2 +- xcmparser/Program.cs | 2 +- xcmparser/pruefstand.xml | 22 +++++++++++--- xcmparser/xcmparser.csproj | 2 +- xcodegen/xcodegen.csproj | 2 +- 14 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 libxcm/JsonTypes/JsonCommand.cs create mode 100644 libxcm/JsonTypes/JsonElement.cs create mode 100644 libxcm/JsonTypes/JsonSymbol.cs diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs index 09cb6fc..65052f4 100644 --- a/libxcm/Connection.cs +++ b/libxcm/Connection.cs @@ -1,9 +1,7 @@ using libconnection; using System; using System.Collections.Generic; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading; +using System.Linq; using System.Xml; namespace libxcm @@ -11,9 +9,14 @@ namespace libxcm public class Connection : StreamPipe { private object _lock = new object(); - public Connection(XmlNode node) + public Connection(XmlNode node, bool reverse = false) { - foreach(XmlNode stage in node) + var iterator = node.GetChildsOrdered(); + if(reverse) + { + iterator = iterator.Reverse(); + } + foreach (XmlNode stage in iterator) { if(stage.NodeType != XmlNodeType.Comment) { diff --git a/libxcm/JsonConverter.cs b/libxcm/JsonConverter.cs index ef9d0a9..6ce1582 100644 --- a/libxcm/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -1,4 +1,5 @@ using libxcm; +using libxcm.JsonTypes; using System; using System.Collections.Generic; using System.Linq; @@ -39,7 +40,7 @@ public static object EntryToExtendedJSON(Entry entry) value = entry.GetValue() }; } - public static string ConvertDataToJSON(Message msg, bool pretty = false) + public static string ConvertDataToJSON(Message msg, bool pretty = true) { Dictionary tags = new Dictionary(); foreach (Symbol symbol in msg) @@ -73,5 +74,22 @@ public byte[] ConvertToByteArray(Message msg) { return ConvertDataToJSONByte(msg); } + + public static void GetDataFromJson(IEnumerabledata, Command com) + { + GetDataFromJson(data as byte[] ?? data.ToArray(), com); + } + + public static void GetDataFromJson(byte[] data, Command com) + { + var str = Encoding.UTF8.GetString(data); + GetDataFromJson(str, com); + } + + public static void GetDataFromJson(string json, Command com) + { + var jsoncommand = JsonSerializer.Deserialize(json); + jsoncommand?.FillCommand(com); + } } } diff --git a/libxcm/JsonTypes/JsonCommand.cs b/libxcm/JsonTypes/JsonCommand.cs new file mode 100644 index 0000000..fd37bd7 --- /dev/null +++ b/libxcm/JsonTypes/JsonCommand.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace libxcm.JsonTypes +{ + internal class JsonCommand + { + public JsonSymbol[] Fields { get; set; } = new JsonSymbol[0]; + public string Type { get; set; } + public string Name { get; set; } + + public void FillCommand(Command command) + { + if(Type.ToLower() == "sendcommand" && Name.ToLower() == command.Name.ToLower()) + { + foreach (var symbol in command) + { + if (symbol.IsAnonymous) + { + + foreach (var element in Fields.TakeWhile(x => x.IsAnonymous).SelectMany(x => x.Elements)) + { + var entry = symbol.FindEntry(element.Name); + if (entry != null) + { + element.FillEntry(entry); + } + } + } + else + { + Fields.TakeWhile(x => x.Name.ToLower() == symbol.Name.ToLower()).First()?.FillSymbol(symbol); + } + } + } + } + } +} diff --git a/libxcm/JsonTypes/JsonElement.cs b/libxcm/JsonTypes/JsonElement.cs new file mode 100644 index 0000000..d4f2d14 --- /dev/null +++ b/libxcm/JsonTypes/JsonElement.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace libxcm.JsonTypes +{ + internal class JsonElement + { + public object value { get; set; } = null; + public string Name { get; set; } + + public void FillEntry(Entry entry) + { + entry.Value.SetValue(((System.Text.Json.JsonElement)value).GetRawText()); + } + } +} diff --git a/libxcm/JsonTypes/JsonSymbol.cs b/libxcm/JsonTypes/JsonSymbol.cs new file mode 100644 index 0000000..86db1dc --- /dev/null +++ b/libxcm/JsonTypes/JsonSymbol.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace libxcm.JsonTypes +{ + internal class JsonSymbol + { + public JsonElement[] Elements { get; set; } = new JsonElement[0]; + public string Name { get; set; } + + public bool IsAnonymous => Name?.ToLower().Contains("anonymous") ?? true; + + public void FillSymbol(Symbol symbol) + { + foreach(var entry in symbol) + { + Elements.TakeWhile(x => x.Name.ToLower() == entry.Name.ToLower()).First()?.FillEntry(entry); + } + } + } +} diff --git a/libxcm/XCMDokument.cs b/libxcm/XCMDokument.cs index 9997786..10bc8d1 100644 --- a/libxcm/XCMDokument.cs +++ b/libxcm/XCMDokument.cs @@ -45,7 +45,7 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory) { throw new ArgumentException("Each connection element must have a unique name"); } - var connection = new Connection(outcon); + var connection = new Connection(outcon, true); outgoingConnections.Add(connName, connection); } diff --git a/libxcm/libxcm.csproj b/libxcm/libxcm.csproj index 842a3af..7cec651 100644 --- a/libxcm/libxcm.csproj +++ b/libxcm/libxcm.csproj @@ -1,7 +1,7 @@  - net5.0 + net7.0 diff --git a/libxcmparse/DataObjects/DataCommand.cs b/libxcmparse/DataObjects/DataCommand.cs index 6702293..af6b3d6 100644 --- a/libxcmparse/DataObjects/DataCommand.cs +++ b/libxcmparse/DataObjects/DataCommand.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.Json.Serialization; using System.Xml; namespace libxcmparse.DataObjects @@ -23,6 +24,15 @@ protected DataCommand(XmlNode commandNode, List knownSymbols, Func message) { int offset = 0; - byte[] msgArray = message as byte[] ?? message.ToArray(); + byte[] msgArray = message.Skip(IDByteLength + IDOffset).ToArray(); foreach (var symbol in this) { if(offset >= msgArray.Length) diff --git a/libxcmparse/libxcmparse.csproj b/libxcmparse/libxcmparse.csproj index 6d63113..964ada4 100644 --- a/libxcmparse/libxcmparse.csproj +++ b/libxcmparse/libxcmparse.csproj @@ -1,7 +1,7 @@  - net5.0 + net7.0 diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 81de88c..17406cd 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -84,7 +84,7 @@ static int Main(string[] args) while(true) { - + Thread.Sleep(1000); } /* diff --git a/xcmparser/pruefstand.xml b/xcmparser/pruefstand.xml index 4c3bb1c..757c6b1 100644 --- a/xcmparser/pruefstand.xml +++ b/xcmparser/pruefstand.xml @@ -5,7 +5,7 @@ serial - COM6 + COM5 smp @@ -14,9 +14,13 @@ + + console + udp - 127.0.0.1:8000 + 10.0.1.131:8000 + 0.0.0.0:7000 @@ -27,11 +31,21 @@ testboardSerial telegrafStream - + - + + + + + + + + + + + \ No newline at end of file diff --git a/xcmparser/xcmparser.csproj b/xcmparser/xcmparser.csproj index c5d2ef1..a5fc92d 100644 --- a/xcmparser/xcmparser.csproj +++ b/xcmparser/xcmparser.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net7.0 diff --git a/xcodegen/xcodegen.csproj b/xcodegen/xcodegen.csproj index 76e595a..33d563f 100644 --- a/xcodegen/xcodegen.csproj +++ b/xcodegen/xcodegen.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net7.0 From 2eee3c3a0fee9460b94e96bb961269ee7fd4c8c7 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Thu, 1 Jun 2023 01:06:44 +0200 Subject: [PATCH 13/27] Add logparsing functionality --- libconnection | 2 +- libxcm/Connection.cs | 3 +- libxcm/JsonConverter.cs | 4 +-- libxcm/Message.cs | 2 +- libxcm/XCMDokument.cs | 9 +++--- libxcmparse/DataObjects/DataMessage.cs | 6 +++- xcmparser/LOGFILE.BIN | Bin 0 -> 419499 bytes xcmparser/Program.cs | 6 ++-- xcmparser/pruefstand.xml | 38 ++++++++++++++----------- xcmparser/xcmparser.csproj | 3 ++ 10 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 xcmparser/LOGFILE.BIN diff --git a/libconnection b/libconnection index 07a840e..8843637 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit 07a840e3696268f4115d7021476f7ae8d935d8cf +Subproject commit 8843637634722680395f391050a5db2e9c10b2d4 diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs index 65052f4..43b1e67 100644 --- a/libxcm/Connection.cs +++ b/libxcm/Connection.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Xml; namespace libxcm @@ -9,7 +10,7 @@ namespace libxcm public class Connection : StreamPipe { private object _lock = new object(); - public Connection(XmlNode node, bool reverse = false) + public Connection(XmlNode node, CancellationToken token, bool reverse = false) : base(token) { var iterator = node.GetChildsOrdered(); if(reverse) diff --git a/libxcm/JsonConverter.cs b/libxcm/JsonConverter.cs index 6ce1582..98a14ef 100644 --- a/libxcm/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -40,7 +40,7 @@ public static object EntryToExtendedJSON(Entry entry) value = entry.GetValue() }; } - public static string ConvertDataToJSON(Message msg, bool pretty = true) + public static string ConvertDataToJSON(Message msg, bool pretty = false) { Dictionary tags = new Dictionary(); foreach (Symbol symbol in msg) @@ -67,7 +67,7 @@ public static string ConvertDataToJSON(Message msg, bool pretty = true) MessageName = msg.Name, Fields = tags, isExtended = true - }, options); + }, options) + "\n"; } public byte[] ConvertToByteArray(Message msg) diff --git a/libxcm/Message.cs b/libxcm/Message.cs index e0196e9..25243b6 100644 --- a/libxcm/Message.cs +++ b/libxcm/Message.cs @@ -136,7 +136,7 @@ public static byte[] GetHex(string hex) foreach (string token in tokenized) { int value = Convert.ToInt32(token, 16); - id.Insert(0, (byte)value); + id.Add((byte)value); } } catch(FormatException) diff --git a/libxcm/XCMDokument.cs b/libxcm/XCMDokument.cs index 10bc8d1..eb6a495 100644 --- a/libxcm/XCMDokument.cs +++ b/libxcm/XCMDokument.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading; using System.Xml; namespace libxcm @@ -12,12 +13,12 @@ public class XCMDokument private Dictionary incommingConnections = new Dictionary(); private Dictionary outgoingConnections = new Dictionary(); - public XCMDokument(XmlDocument doc, ITokenizerFactory factory) : this(doc.DocumentElement, factory) + public XCMDokument(XmlDocument doc, ITokenizerFactory factory, CancellationTokenSource cts) : this(doc.DocumentElement, factory, cts) { } - public XCMDokument(XmlElement root, ITokenizerFactory factory) + public XCMDokument(XmlElement root, ITokenizerFactory factory, CancellationTokenSource cts) { foreach (XmlElement incon in root.SelectNodes("/spacesystem/connections/incomming/connection")) //Read all incomming connections and generate connection elements from it { @@ -30,7 +31,7 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory) { throw new ArgumentException("Each connection element must have a unique name"); } - var connection = new Connection(incon); + var connection = new Connection(incon, cts.Token); incommingConnections.Add(connName, connection); } @@ -45,7 +46,7 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory) { throw new ArgumentException("Each connection element must have a unique name"); } - var connection = new Connection(outcon, true); + var connection = new Connection(outcon, cts.Token, true); outgoingConnections.Add(connName, connection); } diff --git a/libxcmparse/DataObjects/DataMessage.cs b/libxcmparse/DataObjects/DataMessage.cs index 42dd952..fc56262 100644 --- a/libxcmparse/DataObjects/DataMessage.cs +++ b/libxcmparse/DataObjects/DataMessage.cs @@ -37,7 +37,11 @@ public DataMessage(XmlNode messageNode, List knownSymbols, Connection in private void Inbound_MessageReceived(object sender, libconnection.MessageEventArgs e) { - ParseMessage(e.Message); + var msg = e.Message; + if (Match(msg)) + { + ParseMessage(msg); + } } public void ParseMessage(IEnumerable message) diff --git a/xcmparser/LOGFILE.BIN b/xcmparser/LOGFILE.BIN new file mode 100644 index 0000000000000000000000000000000000000000..7872fe4ea4eb2cfca1b287be44a2e69d53999d8c GIT binary patch literal 419499 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 { diff --git a/xcmparser/pruefstand.xml b/xcmparser/pruefstand.xml index 757c6b1..f36f73a 100644 --- a/xcmparser/pruefstand.xml +++ b/xcmparser/pruefstand.xml @@ -2,10 +2,10 @@ - + - serial - COM5 + fileinput + LOGFILE.BIN smp @@ -19,7 +19,7 @@ udp - 10.0.1.131:8000 + 10.0.1.131:8094 0.0.0.0:7000 @@ -27,25 +27,31 @@ - - testboardSerial + + sharkSerial telegrafStream - + - - - - + + + + + + + - - - + - + + + + - + + + \ No newline at end of file diff --git a/xcmparser/xcmparser.csproj b/xcmparser/xcmparser.csproj index a5fc92d..6ed69f6 100644 --- a/xcmparser/xcmparser.csproj +++ b/xcmparser/xcmparser.csproj @@ -23,6 +23,9 @@ + + Always + Always From 0eb117ffc20fde9dbc78b423d53b01c84eb266ad Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Sat, 12 Aug 2023 10:57:26 +0200 Subject: [PATCH 14/27] Update codegeneration to new libxcm --- libxcm/XCMDokument.cs | 1 + libxcm/XCMTokenizer.cs | 2 + xcodegen/FMSXCM.xml | 70 ------------------------ xcodegen/Program.cs | 111 +++++++++++++++++++++------------------ xcodegen/pruefstand.xml | 53 +++++++++++++++++++ xcodegen/xcodegen.csproj | 2 +- 6 files changed, 116 insertions(+), 123 deletions(-) delete mode 100644 xcodegen/FMSXCM.xml create mode 100644 xcodegen/pruefstand.xml diff --git a/libxcm/XCMDokument.cs b/libxcm/XCMDokument.cs index eb6a495..5e896e0 100644 --- a/libxcm/XCMDokument.cs +++ b/libxcm/XCMDokument.cs @@ -61,6 +61,7 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory, CancellationToken if (systemname == null || tokenizers.ContainsKey(systemname)) throw new ArgumentException("A system must have a unique name"); var tokenizer = factory.BuildTokenizer(systemNode, symbols, incommingConnections, outgoingConnections); + tokenizer.Name = systemname; tokenizers.Add(systemname, tokenizer); } } diff --git a/libxcm/XCMTokenizer.cs b/libxcm/XCMTokenizer.cs index 6ebad16..5818539 100644 --- a/libxcm/XCMTokenizer.cs +++ b/libxcm/XCMTokenizer.cs @@ -46,6 +46,8 @@ public XCMTokenizer(XmlNode root, IEnumerable symbols, Dictionary connections) { var defInput = node.SelectSingleNode("defaultInput"); diff --git a/xcodegen/FMSXCM.xml b/xcodegen/FMSXCM.xml deleted file mode 100644 index 5e2ed21..0000000 --- a/xcodegen/FMSXCM.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/xcodegen/Program.cs b/xcodegen/Program.cs index d8b3927..b2d171d 100644 --- a/xcodegen/Program.cs +++ b/xcodegen/Program.cs @@ -11,6 +11,7 @@ using System.Xml.Schema; using System.Reflection; using CommandLine; +using System.Threading; namespace xcodegen { @@ -18,12 +19,12 @@ class Options { [Option('f', "xcmfile", Required = true, HelpText = "The xcm file with the package definitions")] public string Xcmfile { get; set; } - [Option('t', "templatefolder", Required = false, HelpText = "The folder that contains the templates")] + [Option('t', "templatefolder", Required = true, HelpText = "The folder that contains the templates")] public string TemplateFolder { get; set; } = string.Empty; - [Option('o', "outputfolder", Required = false, HelpText = "The folder where to write the output files")] + [Option('o', "outputfolder", Required = true, HelpText = "The folder where to write the output files")] public string OutputFolder { get; set; } = string.Empty; [Option('n', "filename", Required = false, HelpText = "The basefilename (without extension) for the generated output files", Default = "xcode")] - public string FileName { get; set; } + public string FileName { get; set; } = "xcode"; [Option("swap", HelpText = "Swap messages and commands in the output")] public bool Swap { get; set; } = false; } @@ -100,69 +101,75 @@ static int Main(string[] args) } TypeGenerator typegenerator = new TypeGenerator(Path.Combine(assemblyDir, "cdtypes.xml")); - XCMTokenizer tokenizer = new XCMTokenizer(doc); - int maxmessagesize = 0; - int maxcommandsize = 0; + using CancellationTokenSource cts = new CancellationTokenSource(); - foreach (var message in tokenizer.GetObjects()) + var tokenizerFactory = new XCMFactory(); + var xcmdoc = new XCMDokument(doc, tokenizerFactory, cts); + + foreach(var tokenizer in xcmdoc.GetTokenizers()) { - if (message.GetMaximumByteLength() > maxmessagesize) + int maxmessagesize = 0; + int maxcommandsize = 0; + foreach (var message in tokenizer.GetObjects()) { - maxmessagesize = message.GetMaximumByteLength(); + if(message.GetMaximumByteLength() > maxmessagesize) + { + maxmessagesize= message.GetMaximumByteLength(); + } } - } - - foreach (var cmd in tokenizer.GetObjects()) - { - if (cmd.GetMaximumByteLength() > maxcommandsize) + foreach(var command in tokenizer.GetObjects()) { - maxcommandsize = cmd.GetMaximumByteLength(); + if(command.GetMaximumByteLength() > maxcommandsize) + { + maxcommandsize= command.GetMaximumByteLength(); + } } - } - - object messages; - object commands; - if(opts.Swap) - { - commands = CreateTemplateObject(tokenizer.GetObjects().ToList(), typegenerator); - messages = CreateTemplateObject(tokenizer.GetObjects().Cast().ToList(), typegenerator); - } - else - { - messages = CreateTemplateObject(tokenizer.GetObjects().ToList(), typegenerator); - commands = CreateTemplateObject(tokenizer.GetObjects().Cast().ToList(), typegenerator); - } + object messages; + object commands; - foreach (string filepath in Directory.GetFiles(templateFolder)) - { - if (Path.GetExtension(filepath) == ".sbntxt") + if (opts.Swap) { - string filename = Path.GetFileName(filepath); - string extension = filename.Substring(filename.IndexOf('.'), filename.LastIndexOf('.') - filename.IndexOf('.')); - string outputFileName = opts.FileName + extension; - string template = filepath; - var tmp = Template.Parse(File.ReadAllText(template), template); - var parsed = tmp.Render(new - { - messages, - commands, - maxmessagesize, - maxcommandsize, - outputfilename = outputFileName, - currentdate = DateTime.Now - }); - string f = Path.Combine(outputFolder, outputFileName); - File.WriteAllText(f, parsed); - Console.WriteLine("Generated file: " + f); + commands = CreateTemplateObject(tokenizer.GetObjects().ToList(), typegenerator); + messages = CreateTemplateObject(tokenizer.GetObjects().Cast().ToList(), typegenerator); } else { - string f = Path.Combine(outputFolder, Path.GetFileName(filepath)); - File.Copy(filepath, f); - Console.WriteLine("Copied file: " + f); + messages = CreateTemplateObject(tokenizer.GetObjects().ToList(), typegenerator); + commands = CreateTemplateObject(tokenizer.GetObjects().Cast().ToList(), typegenerator); } + + foreach (string filepath in Directory.GetFiles(templateFolder)) + { + if (Path.GetExtension(filepath) == ".sbntxt") + { + string filename = Path.GetFileName(filepath); + string extension = filename.Substring(filename.IndexOf('.'), filename.LastIndexOf('.') - filename.IndexOf('.')); + string outputFileName = opts.FileName + "_" + tokenizer.Name + extension; + string template = filepath; + var tmp = Template.Parse(File.ReadAllText(template), template); + var parsed = tmp.Render(new + { + messages, + commands, + maxmessagesize, + maxcommandsize, + outputfilename = outputFileName, + currentdate = DateTime.Now + }); + string f = Path.Combine(outputFolder, outputFileName); + File.WriteAllText(f, parsed); + Console.WriteLine("Generated file: " + f); + } + else + { + string f = Path.Combine(outputFolder, Path.GetFileName(filepath)); + File.Copy(filepath, f); + Console.WriteLine("Copied file: " + f); + } + } + } return 0; } diff --git a/xcodegen/pruefstand.xml b/xcodegen/pruefstand.xml new file mode 100644 index 0000000..aad72c5 --- /dev/null +++ b/xcodegen/pruefstand.xml @@ -0,0 +1,53 @@ + + + + + + + smp + + + + + + + console + + + udp + 10.0.1.131:8094 + 0.0.0.0:7000 + + + + + + + + sharkSerial + telegrafStream + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/xcodegen/xcodegen.csproj b/xcodegen/xcodegen.csproj index 33d563f..4c54ba0 100644 --- a/xcodegen/xcodegen.csproj +++ b/xcodegen/xcodegen.csproj @@ -25,7 +25,7 @@ Always - + Always From 6e136a7af4a01a800ca51a8f2691d7dabc2c2929 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Fri, 10 Nov 2023 09:19:22 +0100 Subject: [PATCH 15/27] Basic functionality update Adds new connection methods Adjust json converter output for simplified telegraf message handling --- libconnection | 2 +- libxcm/Connection.cs | 9 +++++++-- libxcm/IMessageConverter.cs | 1 + libxcm/JsonConverter.cs | 22 ++++++++++++++++++---- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/libconnection b/libconnection index 8843637..c7cb5df 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit 8843637634722680395f391050a5db2e9c10b2d4 +Subproject commit c7cb5dfd9a7ce720c4850d073ec384ae03844bf9 diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs index 43b1e67..49609d7 100644 --- a/libxcm/Connection.cs +++ b/libxcm/Connection.cs @@ -63,8 +63,13 @@ public Connection(XmlNode node, CancellationToken token, bool reverse = false) : public void TransmitData(Message msg) { - var data = MessageConverter.ConvertToByteArray(msg); - TransmitMessage(new libconnection.Message(data)); + object additionalData; + var data = MessageConverter.ConvertToByteArray(msg, out additionalData); + var convertedMsg = new libconnection.Message(data) + { + CustomObject = additionalData + }; + TransmitMessage(convertedMsg); } public void TransmitDataSynchronized(Message msg) diff --git a/libxcm/IMessageConverter.cs b/libxcm/IMessageConverter.cs index 9031362..ba2d74e 100644 --- a/libxcm/IMessageConverter.cs +++ b/libxcm/IMessageConverter.cs @@ -9,5 +9,6 @@ namespace libxcm public interface IMessageConverter { public byte[] ConvertToByteArray(Message msg); + public byte[] ConvertToByteArray(Message msg, out object additionalData); } } diff --git a/libxcm/JsonConverter.cs b/libxcm/JsonConverter.cs index 98a14ef..792e0a7 100644 --- a/libxcm/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -28,9 +28,9 @@ private static object SymbolToTagedObject(Symbol symb) }; } - public static byte[] ConvertDataToJSONByte(Message msg) + public static byte[] ConvertDataToJSONByte(Message msg, ref string msgName) { - return Encoding.UTF8.GetBytes(ConvertDataToJSON(msg)); + return Encoding.UTF8.GetBytes(ConvertDataToJSON(msg, ref msgName)); } public static object EntryToExtendedJSON(Entry entry) @@ -40,7 +40,7 @@ public static object EntryToExtendedJSON(Entry entry) value = entry.GetValue() }; } - public static string ConvertDataToJSON(Message msg, bool pretty = false) + public static string ConvertDataToJSON(Message msg, ref string msgName, bool pretty = false) { Dictionary tags = new Dictionary(); foreach (Symbol symbol in msg) @@ -61,6 +61,7 @@ public static string ConvertDataToJSON(Message msg, bool pretty = false) JsonSerializerOptions options = new JsonSerializerOptions(); options.Converters.Add(new BigIntegerConverter()); options.WriteIndented = pretty; + msgName = msg.Name; return JsonSerializer.Serialize(new { Type = "receiveddata", @@ -72,7 +73,20 @@ public static string ConvertDataToJSON(Message msg, bool pretty = false) public byte[] ConvertToByteArray(Message msg) { - return ConvertDataToJSONByte(msg); + string dummy = string.Empty; + return ConvertDataToJSONByte(msg, ref dummy); + } + + public byte[] ConvertToByteArray(Message msg, out object additionalData) + { + string name = string.Empty; + additionalData = null; + var output = ConvertDataToJSONByte(msg, ref name); + if(!string.IsNullOrWhiteSpace(name)) + { + additionalData = name; + } + return output; } public static void GetDataFromJson(IEnumerabledata, Command com) From 13244786003dc5565e530921e9c14f20991ddc8b Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Tue, 14 Nov 2023 16:20:21 +0100 Subject: [PATCH 16/27] Cleanup --- libconnection | 2 +- xcmparser/Program.cs | 12 ++++++------ xcmparser/TelegrafUDPStream.cs | 26 ++++++++++++++------------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/libconnection b/libconnection index c7cb5df..026b54b 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit c7cb5dfd9a7ce720c4850d073ec384ae03844bf9 +Subproject commit 026b54b700fdc9ba189ecbc9b60122a59f85985b diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 59e526b..74968fc 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -62,12 +62,12 @@ static int Main(string[] args) if (optionsValid && opts != null) { - XmlReaderSettings settings = new XmlReaderSettings(); + XmlReaderSettings settings = new(); settings.Schemas.Add("http://www.w3schools.com/", Path.Combine(assemblyDir, "xcm.xsd")); settings.ValidationType = ValidationType.Schema; XmlReader reader = XmlReader.Create(opts.Xcmfile); - XmlDocument doc = new XmlDocument(); + XmlDocument doc = new(); try { doc.Load(reader); @@ -179,7 +179,7 @@ static void CommandEditStateMachine(XCMParserTokenizer tokenizer, DataStream str var commands = tokenizer.GetObjects(); IEnumerable> enrylist = null; DataCommand selectedCommand = null; - KeyValuePair selectedEntry = new KeyValuePair(null, null); + KeyValuePair selectedEntry = new (null, null); try { while (running) @@ -338,15 +338,15 @@ static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, { msg.ParseMessage(processData.Skip(msg.IDByteLength + msg.IDOffset)); ret = true; - + string name = string.Empty; if (options != null && options.Verbose) { - var json = JsonConverter.ConvertDataToJSON(msg, true); + var json = JsonConverter.ConvertDataToJSON(msg, ref name, true); Console.WriteLine(json); } if (options == null || !options.NoForward) { - var json = JsonConverter.ConvertDataToJSON(msg); + var json = JsonConverter.ConvertDataToJSON(msg, ref name); jsondata = json; stream.TransmitMessage(new libconnection.Message(Encoding.UTF8.GetBytes(json))); } diff --git a/xcmparser/TelegrafUDPStream.cs b/xcmparser/TelegrafUDPStream.cs index a1ae764..4dae5cd 100644 --- a/xcmparser/TelegrafUDPStream.cs +++ b/xcmparser/TelegrafUDPStream.cs @@ -21,9 +21,9 @@ internal class TelegrafUDPStream : IDisposable { private readonly UdpClient client; - private Task task; - private CancellationTokenSource cts = new CancellationTokenSource(); - private ConcurrentQueue receivedData = new ConcurrentQueue(); + private readonly Task task; + private readonly CancellationTokenSource cts = new (); + private readonly ConcurrentQueue receivedData = new (); private bool isDisposed = false; public event EventHandler ReceivedData; @@ -31,9 +31,10 @@ public TelegrafUDPStream(IPEndPoint Receiveendpoint, IPEndPoint transmitEndpoint { client = new UdpClient(Receiveendpoint); client.Connect(transmitEndpoint); + var token = cts.Token; task = Task.Factory.StartNew(async () => { - while (!cts.IsCancellationRequested) + while (!token.IsCancellationRequested) { try { @@ -53,7 +54,7 @@ public TelegrafUDPStream(IPEndPoint Receiveendpoint, IPEndPoint transmitEndpoint } } - }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } ~TelegrafUDPStream() @@ -63,24 +64,23 @@ public TelegrafUDPStream(IPEndPoint Receiveendpoint, IPEndPoint transmitEndpoint public byte[] ReadMessage() { - byte[] ret; - if(receivedData.TryDequeue(out ret)) + if (receivedData.TryDequeue(out byte[] ret)) { return ret; } else { - return new byte[0]; + return Array.Empty(); } } public async Task ReadMessageAsync() { - TaskCompletionSource tcs = new TaskCompletionSource(); - EventHandler f = (sender, args) => + TaskCompletionSource tcs = new(); + void f(object sender, EventArgs args) { tcs.SetResult(ReadMessage()); - }; + } ReceivedData += f; byte[] ret = await tcs.Task; ReceivedData -= f; @@ -89,7 +89,8 @@ public async Task ReadMessageAsync() public void SendData(DataMessage msg) { - byte[] data = JsonConverter.ConvertDataToJSONByte(msg); + string name = msg.Name; + byte[] data = JsonConverter.ConvertDataToJSONByte(msg, ref name); client.Send(data, data.Length); } @@ -103,6 +104,7 @@ await Task.Run(() => public void Dispose() { + GC.SuppressFinalize(this); if(!isDisposed) { cts?.Cancel(); From f9b998191f4eee8f5dd0c9a678c496f4dff90f60 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Tue, 14 Nov 2023 18:22:28 +0100 Subject: [PATCH 17/27] Beginn adding multi system support --- libxcm/Command.cs | 2 +- libxcm/JsonConverter.cs | 1 + libxcm/Message.cs | 6 ++++-- libxcm/XCMTokenizer.cs | 17 +++++++++-------- libxcmparse/DataObjects/DataCommand.cs | 3 ++- libxcmparse/DataObjects/DataMessage.cs | 3 ++- libxcmparse/XCMParserTokenizer.cs | 8 ++++---- 7 files changed, 23 insertions(+), 17 deletions(-) diff --git a/libxcm/Command.cs b/libxcm/Command.cs index 8e96cc5..3999f28 100644 --- a/libxcm/Command.cs +++ b/libxcm/Command.cs @@ -7,7 +7,7 @@ namespace libxcm { public class Command : Message { - public Command(XmlNode commandNode, List knownSymbols, Connection inbound, Connection outbound) : base(commandNode, knownSymbols, inbound, outbound) + public Command(XmlNode commandNode, List knownSymbols, Connection inbound, Connection outbound, string systemName) : base(commandNode, knownSymbols, inbound, outbound, systemName) { } diff --git a/libxcm/JsonConverter.cs b/libxcm/JsonConverter.cs index 792e0a7..66d6d32 100644 --- a/libxcm/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -64,6 +64,7 @@ public static string ConvertDataToJSON(Message msg, ref string msgName, bool pre msgName = msg.Name; return JsonSerializer.Serialize(new { + msg.SystemName, Type = "receiveddata", MessageName = msg.Name, Fields = tags, diff --git a/libxcm/Message.cs b/libxcm/Message.cs index 25243b6..8c65706 100644 --- a/libxcm/Message.cs +++ b/libxcm/Message.cs @@ -28,9 +28,11 @@ private static Entry EntryFactory(XmlNode entryNode) return new Entry(entryNode); } - public Message(XmlNode messageNode, List knownSymbols, Connection inbound, Connection outbound) : this(messageNode, knownSymbols, SymbolFactory, EntryFactory, inbound, outbound) + public string SystemName{get;set;} + + public Message(XmlNode messageNode, List knownSymbols, Connection inbound, Connection outbound, string systemName) : this(messageNode, knownSymbols, SymbolFactory, EntryFactory, inbound, outbound) { - + SystemName = systemName; } protected Message(XmlNode messageNode, List knownSymbols, Func symbolFactory, Func EntryFactory, Connection inbound, Connection outbound) diff --git a/libxcm/XCMTokenizer.cs b/libxcm/XCMTokenizer.cs index 5818539..fdb2db0 100644 --- a/libxcm/XCMTokenizer.cs +++ b/libxcm/XCMTokenizer.cs @@ -28,6 +28,7 @@ public XCMTokenizer(XmlNode root, IEnumerable symbols, Connection inboun { AddKownSymbols(symbols); } + string systenName = root.Attributes["name"]?.Value ?? ""; foreach (XmlNode token in root) { if (token.NodeType != XmlNodeType.Comment) @@ -36,7 +37,7 @@ public XCMTokenizer(XmlNode root, IEnumerable symbols, Connection inboun { if (t.NodeType != XmlNodeType.Comment) { - BuildToken(token, t); + BuildToken(token, t, systenName); } } } @@ -104,14 +105,14 @@ virtual protected void AddKownSymbols(IEnumerable symbols) } } - virtual public Message BuildMessage(XmlNode node, Connection inbound, Connection outbound) + virtual public Message BuildMessage(XmlNode node, Connection inbound, Connection outbound, string systemName) { - return new Message(node, knownSymbols, inbound, outbound); + return new Message(node, knownSymbols, inbound, outbound, systemName); } - virtual public Command BuildCommand(XmlNode node, Connection inbound, Connection outbound) + virtual public Command BuildCommand(XmlNode node, Connection inbound, Connection outbound, string systemName) { - return new Command(node, knownSymbols, inbound, outbound); + return new Command(node, knownSymbols, inbound, outbound, systemName); } virtual public Symbol BuildSymbol(XmlNode node) @@ -124,7 +125,7 @@ virtual public MessageGroup BuildGroup(XmlNode node) throw new NotImplementedException(); //return new MessageGroup(node, knownSymbols, this); } - virtual protected void BuildToken(XmlNode parent, XmlNode node) + virtual protected void BuildToken(XmlNode parent, XmlNode node, string systemName) { switch(node.Name.ToLower()) { @@ -142,14 +143,14 @@ virtual protected void BuildToken(XmlNode parent, XmlNode node) { compiledtokens.Add("message", new List()); } - compiledtokens["message"].Add(BuildMessage(node, inbound, outbound)); + compiledtokens["message"].Add(BuildMessage(node, inbound, outbound, systemName)); break; case "command": if (!compiledtokens.ContainsKey("command")) { compiledtokens.Add("command", new List()); } - compiledtokens["command"].Add(BuildCommand(node, inbound, outbound)); + compiledtokens["command"].Add(BuildCommand(node, inbound, outbound, systemName)); break; /* case "Group": diff --git a/libxcmparse/DataObjects/DataCommand.cs b/libxcmparse/DataObjects/DataCommand.cs index af6b3d6..7331c44 100644 --- a/libxcmparse/DataObjects/DataCommand.cs +++ b/libxcmparse/DataObjects/DataCommand.cs @@ -9,8 +9,9 @@ namespace libxcmparse.DataObjects { public class DataCommand : Command { - public DataCommand(XmlNode commandNode, List knownSymbols, Connection inbound, Connection outbound) : this(commandNode, knownSymbols, DataMessage.SymbolFactory, DataMessage.EntryFactory, inbound, outbound) + public DataCommand(XmlNode commandNode, List knownSymbols, Connection inbound, Connection outbound, string systemName) : this(commandNode, knownSymbols, DataMessage.SymbolFactory, DataMessage.EntryFactory, inbound, outbound) { + SystemName = systemName; } protected DataCommand(XmlNode commandNode, List knownSymbols, Func symbolFactory, Func EntryFactory, Connection inbound, Connection outbound) : base(commandNode, knownSymbols, symbolFactory, EntryFactory, inbound, outbound) diff --git a/libxcmparse/DataObjects/DataMessage.cs b/libxcmparse/DataObjects/DataMessage.cs index fc56262..e86fb37 100644 --- a/libxcmparse/DataObjects/DataMessage.cs +++ b/libxcmparse/DataObjects/DataMessage.cs @@ -19,8 +19,9 @@ public static Entry EntryFactory(XmlNode entryNode) { return new DataEntry(entryNode); } - public DataMessage(XmlNode messageNode, List knownSymbols, Connection inboundConnection, Connection outboundConnection) : base(messageNode, knownSymbols, SymbolFactory, EntryFactory, inboundConnection, outboundConnection) + public DataMessage(XmlNode messageNode, List knownSymbols, Connection inboundConnection, Connection outboundConnection, string systemName) : base(messageNode, knownSymbols, SymbolFactory, EntryFactory, inboundConnection, outboundConnection) { + SystemName = systemName; List sibblings = new List(); foreach (DataSymbol symb in this) { diff --git a/libxcmparse/XCMParserTokenizer.cs b/libxcmparse/XCMParserTokenizer.cs index 8aad73d..a3bb1b9 100644 --- a/libxcmparse/XCMParserTokenizer.cs +++ b/libxcmparse/XCMParserTokenizer.cs @@ -25,14 +25,14 @@ public override Symbol BuildSymbol(XmlNode node) return new DataSymbol(node); } - public override Command BuildCommand(XmlNode node, Connection inbound, Connection outbound) + public override Command BuildCommand(XmlNode node, Connection inbound, Connection outbound, string systemname) { - return new DataCommand(node, knownSymbols, inbound, outbound); + return new DataCommand(node, knownSymbols, inbound, outbound, systemname); } - public override Message BuildMessage(XmlNode node, Connection inbound, Connection outbound) + public override Message BuildMessage(XmlNode node, Connection inbound, Connection outbound, string systemname) { - return new DataMessage(node, knownSymbols, inbound, outbound); + return new DataMessage(node, knownSymbols, inbound, outbound, systemname); } } } From 814e5d6415404706c4ea075f24aaad82ac8153ec Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Tue, 14 Nov 2023 18:22:49 +0100 Subject: [PATCH 18/27] Remove some old files --- xcmparser/FMSXCM.xml | 305 --------------------------------- xcmparser/LOGFILE.BIN | Bin 419499 -> 0 bytes xcmparser/TelegrafUDPStream.cs | 127 -------------- xcmparser/WebSocketStream.cs | 132 -------------- xcmparser/pruefstand.xml | 57 ------ 5 files changed, 621 deletions(-) delete mode 100644 xcmparser/FMSXCM.xml delete mode 100644 xcmparser/LOGFILE.BIN delete mode 100644 xcmparser/TelegrafUDPStream.cs delete mode 100644 xcmparser/WebSocketStream.cs delete mode 100644 xcmparser/pruefstand.xml diff --git a/xcmparser/FMSXCM.xml b/xcmparser/FMSXCM.xml deleted file mode 100644 index ecd36f5..0000000 --- a/xcmparser/FMSXCM.xml +++ /dev/null @@ -1,305 +0,0 @@ - - - - - - ms - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - m - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/xcmparser/LOGFILE.BIN b/xcmparser/LOGFILE.BIN deleted file mode 100644 index 7872fe4ea4eb2cfca1b287be44a2e69d53999d8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 419499 zcmeGFd9;t!8~=~rUN2L~Tx7~vI5#O{IfLAi453j{2}z=eP*LV&2$57OGNjp*q;rnB zgbWdik|||~LQ&*y`+JF4+b=K#sb=G^Wrz`KL=id9;*S_|(ulquI zFNb$s%)98%%cCz8_hM?2-gi>^<3)$1uUI!E?V)u7$GzI}Uw*w|Z0ex2y359|Je~fd zccHMCQ{ln>fF{$%`N2DjFO>3fUF7d?bPa!)tSJcSxnjW6q9T7evm$X z<&)|4Ul@{BJt>aCp$FRRvJe9ERA$_{wIbpx7=Sd|o1&6lm&4YX+Yvai|= z5|ehDdK1-d@@1+W@UZ2hQ};f-EptuzS{ZvLs&>HBYYk}fdk#O?(!o)?g%);q z{Hg2~i3#(ramRR0twk(b_Xol74NRvVS;w;uCJrs zxW+ZA9q_q#d#B!2@X}=|`F3PnyF#@)C~!0JcRzlW6ny@jqjo1P?6x&icBjOId3Wmk z9qG*r3{9&H>9_~nrQrjqO&1pvZ(Jy2&v|8cTHrRqZqwSNplHi%%`RvBzc)J268SD% zCi^TV?6GLanhh>oQ1QT@DPA+RYuDU}mwE3g6)%^-=VYU;H^>iO*zG7@aSOdyD(lc< zf;(oT9dTqx+Mnf=9qIz&-!hkof^MkYnj@m_=U0~C0iE7tGOjxtH6xQtV4^=y0 zv|B%;$};(*j27SNyt_%@s@D!^a{72uFl3mcc6V6Vm3U3rVZz7~;4!jadCCsBeV2x* zlS<}XCV!OC?3l8q*rP6IO*;vsZkt+5r;qWrMNcH1ZFY+++D*x++F@c2 zw0M-hFYCPfMBoue+RDM=WF`xy`*PI|cvj5;O)B2x2b)tIwTm{p)fVlxuUGBXh)KR3 zGt<$JeKu{iY6mPql}4ZPzZqShQtg0^b{`LP)GpfWwpp~h>!51)t(dUK5~DEh_9dxy zz~f%|$av%AZF^Na;Jh;LR^8(V&o^?^F52u4ShSlmS+zSTCd@muBN=e)O4SaS1m4nH ze2qTqELQD+jdovpj@m_=-6@N9moHWAPK!xm6wotOwF5S5c1({$8D&0H?SR)N4QO(5 zwjZ>++flomIWL~7xc9WCDsqVl>`W4BLRP8*ocv;ic||q9_(3nF2W(Om=fB9_^om>5 z+qFx__OdMX%(>_NPjzg8|M;er=6NdZdMc&oEENxUtoW2$oB2VrBuDY0&F-8KJ%Qxe*tL!j=8Nckb2Nx?l zU{=M%W9KqS9l!UUvMVR>PKl0R_&X_B`LW~3Mw?v?i+1-kQtdEdWKEPZtb%F>eD2-f zQ;xPRDLd`9#9hxu>0;YWrs;(>^^vrvI8zUtWoOQH~pz2 zdw7$wOBMKl_`CO$#osk_9NB2IYh}^y)fuWCCMJW&n(fv}wF4&4bK;$=*l5!~UZUFF zD)211P+6Ff6g>NvqjvXM*j+JR*qN<(%?(ifZ?mz#U{KEBEo*1vjUG5?LOa^6#Vgtqju3|H`b!v z&d#dcI5A@Z1gD?8Z}SNTB)*O84jyALhel}uCZFfma8 zBl{hxv$zMm>+G|dtB`0QqwMRtDn3fkuFPr2kcXI ze8Uy9m0hO5Q)SIo*_#y1dL&!3``V)2&L8!>jR_HyiH%>%I&XuMN+ESDRUau|u2Jze z3T%Sofju3?`@ur59mRGr4-=?~-%yv*y@pP_A4B+mH7WVmD?8u~5~Mt`Bq``tGF!7d zX3=iy5Y-M7Gqfz)d2g$BzX?oYnMg5WqFUa0&lFsp(Ctz3eptPWQNaBw@dOfPj|)Du72ximY5NKPdQ&qjvXM*!}pTvb#Tl z9UkS(1RYsmirlWqdHFKD(oIv99q?pfH!AK275Zgcb`|6mLr;r#j@XHK+z z#;SP0>3xgo{Wf`?xs6pkU?bjxu9=iU7J6@P(V=}dfgM9TrMa>Lrs$Z;RH}`7y|3(m z&5gzTH#rV%wAqcbX!q8Gs@;oXVjLa;UZ-%;0+X91NTF`_z!j<;@EKVaxBe-?@ia&6 zqRs9Ni*{GGR_)%*($1`klwvyXfXR^^+#J(f#rZH-@s_}SWwUsug&!*Ec9|A-Z(XkJFkve*BYPZ2 z6Ze4G%E;i64XAg$vRjtGE>H7p&2FPbyFK5jc9 z;|HZ&{av)#{bti{q-uwWIrgC4q*rxhj|?d)eMg{NU^j*_>a&JQqK5Py1Q* z!z59^85LB&LIN|oS(*2YKbq_40(XtJ+;DCOms%zhGqy zdswvtHrZq9KK8w++5sOGuQIKnAH4O4qju3|SJR^1?$1=aYsADv0z{yNj;eORR95Vt zU7BT^QZ-q%tCgT#orfK@OS7??tn4r`k3kvPU13EJFzE{3_fYrIF~72FB=A~U6~oH; z!E=?eHM`ap?WTp*_?Va^9=B!tR_MqAv)9Ufo>ugH`HO1TM&PRQuB=OcKbY=%hx~wr zU77GD1184bG4KAGs_cNNU0`HM&t5WI*>x1Sk+7@Z*AJ?XcO2PhvwPg4-R`j79TQU> zfOfl+RXbqPWGVSd@D?<+>k!qhm%xi;r)_!RLQ?R?AxH6^v(W1hK4rkf3@xF+x4ip_ zd%#@N@(LMu{8?CrH#m{rv{~7k-gt|8_k|DNFfl0s)XU46DDDB{@_0&%%bQ(S2X}(N zcgR&}rNc=r^{n0)Z_RcV*HQVsDr~o-S}V zaVp0i^MiaZJ8Bngb{|``D;qw`!-Q4K++(XP6=rCG>6OCG08h(K{jKwEp1{M!smzxA z@yowEYPZb7?u{clvY5b*P=(DbExhvqrZDBMg2yro)>x3S{C8!yT;Prp;+4PE51PB) zB1fCuCX04IR95XUK{tNcF7|(*t75ajJQvt?_TS7ZxpQQ`F+sHhHrjpS+7)7th24Gm zlpQ9<-!bnF%~p0l35>sEx8O!8?Wm;db_18BaSxcInbh>Ko257Y zLe&E{kw9D5ib+iF?32r(N*IHJU8zUsbgO?kirU_IN)y z~!Yk#jFx0#f7It5TPmmrI6V@#Aju@|f zc%cW(QxGna$sniwsv`?LOGfs5S3h{&wYfpG+4Zt$ck{KnDlp+WtvUIus;{e}x4>K^ z-@o({ZI{yKR@DyJlSJ8H4g8?WJ&vm)+Uy2fv}-yqO}A*5`$wI3GsMK~vv}jJB~&}$gu3mF*5QR7a5KsDe0C%$I5ymI-bI_; zJd1WCPpEeD#l&3skm+eg&O7b_Q%^>vAUVdr`|G>|J|kYqTLta zQ;-#6V&)yaQ*T+R+5rq+QF~J*C z$B0J>YAQS6HsfANp^1=qVmIIo5~J)$laY08ND^&!KUuUJ*-^FID<+JrsR4a_ zhH3{a*)i?xO62}~c&P$xwCkAcII_`Zcfz7w?psy6-^GM^hkkgJAD&R{fbqt()*=eH z=b)|%;GbmPU6$;I*%lA-fdyBuO-E~Hqq8Q-dQz(k*`PFK}ZyiVam z?svT`nA3%qt=$W+x*@_GozO0S+_Um&&{wpK>V7;T08l zpzLH5s`|kj%N@0gHoNi`?bbY?+Eow}PCcw!PK<>&>AV9b|H*lle#h4r&^238;97EL zv9hoq6!^zcyJ)k!)}|efE#_fj)-2KIVAAn%g+7-!LY)9=Lw6I&VPuXE&qEGhOr&cPvP688s zQpQM(vU`lqJK$5|@8(P7USeprX4l)I-Ax-*yC=kiku{l$hUZi}VD3>U{GgS~M_;LS zz|*Ah*XsBEpw(eV?S@#`HKE2h=3!#IF;T!vCzah$f!WEJcamkyTop!8z$YYvDlTPs zyH{pwb`veyt>(o?%zH^phyqOHzJtIjt}q4p^i1P^dtPrHS*(qAmEw-tMVs9Wi*`9H zsdjIRNus|ya+PWaT(*6eREgR(_c^bkYBy8h9uj?am8y^5T}#QL&2GL$yWy9rb_>J= zZ^FD|&0hVWY6nb1VIp_=-^@jit9AyDb*&noz+T^S%)i6G0K*JwP!?Twxx9 zJUOX>X2>etquOm1Yx6CT9g7^bi#EHx7VU;}nHcl-iHV7gSrw~yt9HOtFp`7f<~?_) z-F|^f%Dh`w*ALpeo-3lw?stoJITxySe~5{hcf@yto>J|AX#y^hfbL|=p4E8={EI{Z zm879tx@!(9+U#=VyLcXDWlXh;i3#)0+@_TNPPGFjtBp72Jlmz8Y6onzJKM`~%|@GD z5sP+>ebuh0n3!_~Yc@0N_W?}LimqLMmPdo}^s&2CyJ7;Lk|^Mj4u0@jH%INF&8~t) zyP@}~b`{0MtXZ_H{fo{!U`~uYLnc7jK1sE!B=Bm{Ze=GwxT?FOcGp_i#ri0_>%@cz zfK|ax_TX>I4j6Bo_k(GftgNJKlwECs&qyvHLq2`fd_uNnccVqS#_6hEV=*zYG1(&erTY*|ZC*12JLEnp{Ae@S_L7_&d6Ou_{J|Irq*2PnH;^a}~*b z{^+RP6Bc%j!}>c+7+G_!C_h-&EHIV$j4XfrMQ0t^J_4KPjPG{L*6fB_vYLl$jzZ(FpBhwlV1F?$73K;a6i-AsY|XI`mIXJtyJ zj98`f4%lcn<~_%Gx4^<~Solt0VHP{HD%Mm}cECgdWEV9GD5>m#O}meqAIjG3R$8=M z6~66RB_<}m<4ktuxM~NyW9B@q(qe>v?5NrSpOG_Jt{WuF)5TG{Z!GKrin1kBAtt7F z5r6kbc-OO4V6xiOkWu$JHSE*}JhBs^zqNd6A@P<0Fz@EGS88tsCDE^$Ic7Isd#TV(wo)i6%&s)&_U?SMIx@c~3}g&DoWyKUeCGO~Ys zoD|$Q*ipM^vny)R&fBKi6-&^LRgqp@wFBn%m?%I_KN(HKX2-ydbawh97U!@PObJQ-{>>69N^O~r3 zftYXuigrZqzm3;<2aI;qqj1-=@^jS=c!^Y{{<=FUDCOF(H`?rOvuHQCyJ~m4n3z)! z^X|v?svWT8Pc*(`uW1)XP{2mJgKs*{yG|B%jY=!K&SJvI5(&WW5XB5}515;%hI=+i z6PNKRZ-pOZ=pwNB(C{1IW@~nRY}(aR?J!|v36t=~oeQaUz}#&U1rQsbou#XyufUVV z8<&2>5Bd~$)GpfWhFP?`F>IKI3EGi6X5MXSq1pj6@6e9hl+^G&*>Hi)OTj{}F9oB` zZjwd26=6F!OcFiHi(z#jaHqmkQ^^}srFBEt4PvsurW$&?YaV5$gxMnJ%&2FJZyCHw6c9LJOo|Eg7+ym6%WEsW5H6;D@JK@4xJT)&7VF6p zx^Ml!4+@vd*6g-gw7W6vn~I59w1iMQXR3CjSy<^AFACpfpf~xPP*I=-nqq5 zyL}dRIlAb?+b<@#Vh$EuEelk;;sTTA z8Pg+=e!ORNB^_DdbV<#gzR?e^$nUr+qRp<7MY{s`sCJm39l=z z$5jz+cDGx!YdJ-=!^9jJ(QX1a1##~Vf$0v(bqb~A4;5DJfQ@!-d`Im%TiA`IH&x8T z#Kb79*|nD_yDkFLFaUo?GT_T~$`06+8nt&#Ye$=1AB%PcXwM(>Fd>mb6vdid5XQ!R z1tx-`9Ga0GK3}y1?knlVx2pNU`}rJ4c9@0TrB5ll;R);**_PpP8~DuaGyj)_y04M4 z10F7s`|lt5!SRo?HM>a`?Z(}$+D#S{b}_S)o!hUg0+>_zHdznlG6{XcEIJOh^+N%esJFP>p0P7 zx6q>9*LhXDMPg$13Oq`?_jTR@%PCvCFw2z8n4ed*12)>lT)!Z_%EE5km&$H+0y|EO zWg99xVA>hbn1`#VPs8LVuzBwG>3m0z5^Z){E!tg5x3-wKO-vG3#prOa0H%o>-^#_K z+`myr7I>PhieIFyX3>uvwTm{p{TA)MrU`k>J0K=zRj`vCMbfy!lvdK4oC3W5>8c&p zz2uXnlUFANXI(i0RTf+0~Wdfg*Jj%1z`@xa> z9JPx!yUG^rHnQEvJWSXO(2gjeWG&SWm~#aUp_sde=jptwlAzuDWgWGvV_|pe2xW%} zBWq&g-7m;zuj3vtF-rf;J(*+|_sho;3wt@ex`OAG#Br@tNx_;W*_&Pyi+Tma%rPcr zFC%k&^Bx`Bn*^RF#pLA3$hx-*OHzQ_$eFQIO+UD;mZNxgSlGQ7-U(pB*cy+wg`PNZ z?@on_>6wwSy(x_FfIkp+^()Ht_BGj>T^EaXw_c{}0uz&UCrUZdR<(ObU>c=yy)A#- zuzz^&0XEvzc5Q*z*TQZiL3+%4^8aA>0WWLg3bU6bJzr1b&%2+}MT@nG1M*dH9NB2I z8*b5V(-_rmgqW~q&DR;fSgYCr^Jt5P29%MV4!_|CTvFnI-y{ww+1XLMXtR6SrrjLX z4iggxkPXNypY26SI>s@8o0d%)9LZo2NanyR3z>10F5`-n#us!S==3n%zN*cHl--{Klbhw^ zE_mYukLbt(n^Rd4S8p6`cDV{(JR9)UpQ;@ucw?FRdb=Hjx7)b|Zr`P0>XLG6bj`l^ zvT6rBPfoOB0zarc#BpS!&F(UbcDL+Q?Jm#K&dj^g;ZsInJPM~CDxs$DQtg0EL3AnC zCUendSH+^;3-79SS7m8u=3RHL_u?KfckirOvY$`9rrH4;?b^GxajR=#cL}coV_rQm zA-*%Qam~ic4pS#v6ASzcQ0gr<`~~wF5S7jrKq0sNJ0wc3*W>c6W&hI~ma@w>{m$Qg>jQ2(xDS zOj-Ldg9qF}qJZMRCI!hKW@~m2S+u*PglgASOw2w@Y@D)7M;4gMPpT@2pz4HAEP%Jk zsi(+4Nx{N99JPDW!cHD`GVl6{iMfj+3iy1OvIB-4%?&BUf9nRFcff{SOV|69XtNt( z(QecWs@+I2NsNux)8`=W0dpoJawiw?%?Z^G_<-EwbsFpkbzK{2M4R2q7VYxBquNao z6Z{=31CP>$^h(?V=Kh$xwgd>tTfbKAUJ-bzR8_Q;T5{>~UdqdLkr1tsrXF+r_n+?G zccHwO!;3b%IX3M!t9F=}a|Isd-Sw&+FwgO*s^IRueE4+x9f4cQ!=o8b_`y%FIcm4q z!fw>R$_^7Vvg~9p-=pk+O|yRXiqFH(e|#!%J6RQnUh;!CT^|cZo81={?be2$ioyhM zY+~c;VSERSH)d7P^7H!eQ&C?E+)+mM+~e}V_jSjS-ELvm>`EP3Ojxh{vR8}dP1QBKBZ1w3_Su@<&lc@QJ*L`W!m421qTM6msRx)08OxW(bw6M$DNZDaxVieZw`zLkH{v|MHvaH+fjEOfXJKzDr?qqX6Sn#*w z$VQu8?n3{)m4J>ZsM=w|sPc<;4bH1}z}yIsnUz(May+c6$RqG{S+gS^_JdP-9ksjM z!Y)54iI|5;qBlPFld=P*d39F%z~pLmbYx2i+)G9_)>7`?T`NDM&8~_~yRh=}Dly?a z%gB6K+vnuk>Q|;;t zY%1M(q&hTgnhxAi?kk%3ez38+qju3|*VLk&iWKuOVa}O-Ht#6a?q-2GSB!h*BW;&D z;+)PqVDn1g_|J~o-DP1n=1m=0Ow6YQh}=ueP+5sEwD!t@5vQsSVnm?fIUJ(;=#wIhHTu0df zOKhS!cLu#}SZM@oJ_2^!^+j2<+0C(NH=2T{nD>sD;E7GneSQJe4wzaV({3TpLLFI! zCB7>qr|lV^JC1C$*)6tcm(Nq}J{1%83bbRdm=?Cc112W!)2)b}ddluq?SKnN?sNEM zevrfUy<4=|eQD8d9X)tr9wvAcJTlrnQBCI^FcH+mW9RfnVEZZ6Zmq!PHf6W#yxU=6 z*LGYl4^$u=L%ddd&RuV zRXboRK1@sZLIZSUe-YT12&(7tq@c|?NA3Qyu*=8sCgx$17^AEYGZny$Y|HvzW%f?V zmsve*&2vWJZt}w7?KMfkJl9*iXtT>*_~N;MG#X~aygXuJMwZO%I{K%@v*2QS*Tbsl z7S_ySJxxyAwVKN3%3V)Ams{8^3qNXILQL?){IX^{w^nw*WO;ZyLwr~7sLng!GQzIh z-G1=xQpb50ZFW~#w0rJj9oehJBr%WjU|0tQOav9ajV_dNZ)?>Kc)FZtzpf;oQGL}> zyXdm3Ah#*?EZSYTFhs?}hK<66P_xK($G!Rj(;|hwM)JoEJB3#Qz$YYxIw9Zxc*Ese z0uiUjT~t$xddqHA@op9qGxNwotw>h!fK3%y-TRkjd>qyXHcJriPuI-xT^4p}9hKeP zS?pvHXn&b%Ih7qS4We)DRG6Lt8S}&bGQg(Bt%B=~MReIQ@w!^HyKte1iubVCu+_3Q z2n4PPzY_s0TdcNSqLs!yv(>3|6L^+5m8RlUw$E{JD$wh1rN^it7?ad8Ck-p%mJ$1T!9G%B!tp0GQA!)(*RyAVbsr!{NTX{ z9Y;3W>^`+#xRjT^PICWxU$ohM zY0>Uk>c(TuHYjemQ7Iy!ZQFc4C*s)o# zDmH}WX4#713sQ$f8|pPu1=hF(Je=alrOPI*X(Qd(cNA3Qyu*(dT-5D`q-bo%?uR*)=_BZYUliH?4Py+jmE0dKS zaI&N;+8j#?N}tNs?D7=3ctRj0{DyB{F=15A$!8l;bKCU}F&F*1~cIS_(cHP9pL;+|wwvB2B%wEB{f-i0^ounQG*jx+bcKuRB ze+#=oE0x^vGF8A=C}vUX?y7{zV5Td2B~&02y7a&_F=5`BC}2VOv>e!+ zD{$05)KYe@Ca~){AX~GWYtb$_e7ucG;+oyfy?5LLmciC9s*mY$D5K`bs@=N+H`Ni97_`wUVEd-;@ zZmmVTb4-6ZZHo!()$Fr>vi-)rud=k`sU;{JXc0`zvl-__d1SjwAt;nXgBbz zYKIA{f?qy%Alp6K0rQ1nDHGHOD(&A>?Y`-sPxWwAuY)(Jq}T&zOe^ zy0OoaA)7Q`wFBOD_E~L`B91zx_$R8}uL7TwHT!l6Kd4>PQM)r1cFA+3vscW+1a@Yx zcuhKcF|^?Hh~_0I%VI)#rFS-g-Uip2=!nzHTlC`DfWJ%V*kWSNjErsPpHw_xbKfH+ z78$Fzs(6Kc zz3;s12Nh$E+BLASyCHnOjY(o0@Ni*ecfG)Tj6teGm$eyJBC}rjLV&!|M*ZHSzx2xC9eKB+U(j{wEH`32!RP6 zh24U4&n~KwG7yZw7VDCJr-6K!_gEZW@=wwQTDOxS7B zk8D7}D^xpRX#}Cq%6Q~^+}C*r+(^9fy|+o(@fD71Hrnh4ShQQZQ$&h+Pl<^sTp(A` zV3ukJ%#8)#dS%V_TcO$kkCi9LxySm!rNbSyi#EGa7VSRorrN!rB87De%)9CzXiHaM znnQACM7u6oEnUIykl_8L-qOXTtD|@kr#IE2Uc;uU-m6*a5ksNgD&7#pJz!I`P4xcn z6rFjm3EW09+wUKi7Kfgrc<)-+JyS>7VM6?A&a@wGS9b3~L+*RRPxsuBes^Y8P#G-&wS4_>XFb ziJ5oxUl@{BH!R2hUSM)5G+p41_PIs6W`WyeJwJNTH9PyOh21}+mEB=6VW&0Oiu?QN z$O6kNpb#RN}`cI*{3!@UAnp#SZ( zsbQ07;Jo5d?))Yx2=X{;cS$k!@c;ffLsyTOmrqRCEBGac_YzmPafLb24oa)5alr5y zIbjhjostvTf>rQ;8BvD{rf6E zSnL`cN1NUC7VR3muG-xoCg{ezBa?FZBGnGKZ2K=BQn?**$I1Za`RFi3!>f-*IAW zO2{1dfT`y(714(qsK0wg;F1#G)t3`vzrazu(H3@3P12FYBryuO?S5qkOx4G#(^Z#o z7q#h(vKu4tU^!PTf7B1k&dJv7UbAR-HhdzDN#d$#L7{Zq1MbspqDJmK^ZoXH)$VnH zkBUc`CLZNS*Vs7P?B26zcYWB_027m&WmOb>LA85dV4?t41rG;Pzf6-mqOo#%Qd+cQMc_AA2fW<-UhtxPZtiDCH12)<{wZ~DrbryDK zYbd+*VgfU6dx(u+T&(PX$u3ecL*syv*C;#Syb>G#Qr{1zG|ATNzPD)iR9v;&B_^y2 zqus%KEvD@9i;2lW;f=Tatn7fP5Tu1Q=Zc@gw~Pe@ zHth0j&erToS+qOF^pAO^v$W$(#(6g5fNBR!9)+EZ_TOuo>c|3{7KbC&IcgVecGWG~ z)oH2PC1q)6&J|Eu5`eLp|O=ft$#xJ#&p8 zeDa8+c4I8;K6zZ(VPZ~YuzTfIop-=g%;1mdL^5H$vKuR~X*Rd^hHTC5b&Ga&Z&d9t zVMUlY;9U5McACJ9EEO}doHH6MQSE>m$eOMAiXY_Z;;7yG7Ivp=DLYKqW$`GC?6TW* zWIqsCu9J1%vD4mOMA-qG2v7RFx_xLP+U!2JX!m3})eaN9F{6t&ULWRpfO)67q+BkI z-4BH~W$6jpUGLh|HQMafTePdYMYY3(ImfCXO6gi!=iLT@Wjg8yOU0w47s#pe4%pPq zo=kO|chO}>YolEj?LN`x6fw17voPYd37^sKmj4uY%w|Cta6|a`2>6t^l$)OQgN%0^ z#XDl5_vDK@v`3Z3|F3Pp4PWWd0uu(%5P~-#NnvMFwH9_Cj_`vUTvNxhXu7f%QD`%T#u z5EJGd4B0HM3m=yQljx!7gD_xv_yifamIUul-Qfp0J7sHjr7hb1xm>j?BPQrZq6h7+ z3Cnqa8Cg@PmGXg(Y}o|u8r<)wU6O@eJ>HMSJYP&0S##h%TU_TIFk2Z9^EosI`;=V` zfj3C_`A=C1QqE*+cF7j){tU0mFfqp-*6a_&%yAExH*Yj?;i_zir`n|mJYD?VgD?2O zeAjQjN1I(si*}2{^d~0FJ3;}pTT(~01E%$-xqM$2_J?dG@GMDxp6}`h?OZ#6N1I&- zi+1(Gv^ypy8Gv>>*Qj>D%sZO&5e1a(qx0@Qfz211-(TgpW_wuJ{TW_aV8Y09poQJ+ z$CMo~#~z{p*zFyx?0O3PlU!Lmb2cgH^K7Ut3QM)k~cJ(I;jhHu9OcM8sAw85GFgsb+7p*cjf2t!pPT-bu-E-ZM zq~Q8C*_z!ni*|o4RP8W9J4Tl%pc{cz+ymzMHi;Bz)02_>j%xRYz=fsgqp7rox^0@H zb{|;S)nBRXFd;_4tH7=`?~>!*hXV6`K~l#u|1wsD9tF6K#CNIk6*_5b?UoFPHoJ6- zcAtjrc`!jY6G2@+T(!#(7=K5zG}m$h*8#w{)Edf%t)T8RlGYxaug3MxBb zasemaxoR1IJpU>kS>SCF8@K;7DH!hhMXPAD>tNCD_dcrKePTl7&S{$y|%HncCsfjRXbo#J$y+`q-{9% zu+BT+QQ}dyH}iwIYXlW-b^|Ti&G%KiL0Q_dX36qA9bRq&69t&_Y>VquJK*J#3;2Fl zQt)#HM~@P1c4IBt&6}p$jS~|SK@l4l2~XR=tXUemG9`Izi)motJ z-Vl?-$h|cmx{72JwT1U9CdO4io&H*(;im{E!@8>3%fqGw4o~|h8-T|!i6JB zuUrB>TuR$3lpgSEaVaZ1`N4N{vp2mO7WEc}rJps$gd;1b3)HI{iU&-t!h9j`RCrSc zTwYe~o}zv*xrU>7DHe9M!=m0)F=1@^W#*M4@QZuE>=sm!vD2OlXCAO&_vq1V&90S2 zyFOu^)~#Z~$l{L~*%ljAJ78k>o`*iq__+h%c@m|3EDu!rO?T8T+U)MLXm|2O z5h>=~FD8jT<=@v-J78|gOkTTDSn~lqRkYh(S(;UJbJQ-n>?r;0Y0<8BR_W(s3F5KU zb~>Tr0dw6$ARxPNM#sO^9|IQ_e?0MNKWN_9QM^GGdJDs<-e<)m@z7ZBVPyx*J5>Tu zaVg2^)s)?H0{4NqTL_gsdkv4 zo6)XEOtl04q4G$L-g!O~j8N@137jtOcuQwL=(O5VyJ)lf(W2cGf2nqu;8ODKsHDrn zdq%Yb&ilbMt(zsoSTStFyhq?WWZpIZ#1B4x+fln{v-{1W-GY&-9VUru_UKyG4w$x| zJSwAU)lZM`bO7={;q4;ey~CeCQj%!MNWWN>A~r za>LzAY8P_2Hgk(Qzk($%o)f4_;Zw{jBqru2lnvwhWvU-AZ3KA*BGvdQ_wQHvfIU%f z^qYPVd(3fk%URgXt*7j+6ca|5vk&Zcg*WiP)G(g8y^@9kwZm)=Z~+MglJD_@A6!3b z6>WAkE!uSpKc;q#m?WNkUc6RE7Pw%I1u1g=(OZ@Iy>(=PE6aXyq`x1$d7b0Prdrtj z@`#RXnwT)}%x>1~kg@|N7e!-ycC!|5DZ54jPm__Y)zlA8x_-(x+U#0ew5vK>wZnvw zWn~aW4V$9c0i&JC^&AfGeA*;vSHSgCzR_m)fJM8xV^lj#xEWyHk%_8Lq9E=8^RCRi zKH4;1=UqpEH%R^G&0qR~@A_6C+Uy>;XxHs;)eaNmjhS~xIj_Y%V7A%?Zye7gAuug$ z#N12Z0kT)T|Dqoht>@@bp0lv~Wt+0Y#O!3SD|nf*1C|EhspDR$xlCAQHuynDcCf&! z<>-^|NZbHhleZtA#8;QyhuKSyr+Ls&?>`GyG#qacf)+ZGBGhb8S!0l%JbqLFiAY3 z0ICG%bkvbuF0i@H`}@9Z&2FPbyI;e6z@{wi7(TT7mZNUm17^)q)5Dsb8aD0UEO23o zpgw8m2d(-!Y8P#Gdu-aB(p7ch|86o+>_XiJTgDw|De;(PnpzMZ4}xRl8bZqPIF(^|K%I^e65Cb2Y$TEBi)9 z@o>KYt}HQLJE;&TR?<l>fvZ0y(+V&32ccEl;AHY+<|?#sxM zagBUePh~em;98RDIlN1nfFE%j*=VyHZ_(~i3S?s51To=8#Y6$uh0U0O=e)jJ&ldEC zx;*><_e6nx$)!}O?FWTdIBNHnh24=w%5J)tP`+T;-Tt|bEHDKEVb`x=4R_Ynk)0uM z2Vu9t_k-VtWovdHTeM4BquR|A6Cxis-PlDRz>Q$uy?@x1nr76cGPZ} zh28s9pv1i8VuDA3As%H#SiS(v8*WCH63{bY=PI?9`0n(Le$Zu5wr011H{@uc9D;f^C4ZFa{k+PzPiLd-iMCd4PFB-6vH$aA%7 z2h5plN|2=5-SWI@_q)JHrRKfHqNHF+El2I5%Z@5Wuhhj(vz~V>tBMgDlMcY)9i%%> zT>n$b7YI^l4t3K+6)%TapORzGw|^!D`!_m>7jb%gimtFly`Eci=3&B4VX7-CJ*46R zbB9d!ST^B|LZLq{k|180LJs1=uDpfahhb%O1u-!mkD*=sp|S(!ldpI@e9F{Ub!dS* z)|S__vyy_lTt8J3ZFaRR+SLdvqpuYcbMB#m`nY>kJ7Dfq8t&PoFOmBXRqcS2C5Fm# ztshiyef!qP!meldn&Cz#N@U)(LSnC4(G}4g;Hta5a(9xs(KX(7UTmIjC%(s_iWIqhsyZ6o-S@sIf z+xvU!$O6;XmApIIfH`3X57<-(JmUH?Z?J{k2ZM!1%o`#mCWgY_-P=Oh0dr<#ui#|S zI(!idZ2aAovmNJMwAoFtXy<>V+D#M_6GGupYJIEP0aJy?$TG8@U#n{t*yI=6f8?m$ zbPKzqdz9S_F)=$C(ec`Ul^w7QxU%b?`Buu&mz5px9TMPOxRBouhVIVZ^yXRA>+!p) zH(yK={c->BHV@dmkP=s&{6OfBf&Zu8pt6qYEw|9~!}km;vgnxrv`R%CTwn?9Ql_o; zbn5*vQU`aXz?CH-Q0Oi{_&uJj*=@FH_d#JD+AU(j!Z6z16b1sora*v%z|X@~JK$z= zM-~$^vN+^LIdoM3)83%t!CD#~w+)-R{ULC1xhz|e;RjE-KBI{?yBwwe zJH!D!9#icwVcwaeMOMQUU@jW>&n~SV<<~Ho5)-(LXtyxk4_>?5aaBZ{T@j0RkB2Ro zFhM&K@2uI8pXt0SDlpgYd_j_;?b7{p%>ozqpBUSc_&T!bIJ{>9k9`EUo*#%jW)X>7VSP7q^n}6m?W->y&va~bTQF#xjcEHTL73=o$3?!v)hH3{~ zUefNZK1>Qm_Hfj0hK1d46_wrFS?pL9#Kuptg~UBz(oi(*<>Xj7{1!5B9XVIrP*|D? zy2dEcW;frW-D3q*y9Hu`Cq_FWs0!OvJ76LxuHU)tx%NsOS>R^kjdQ;22RkM@j_e8x zyPB(%-AXYr{*Jw3TUe?E%+oD$0i-H2t13HS!)~tY=X|5hZi_{`kB+H!--rqRj*9`3 zvrUid$O1F6w29*5wCnTtXtRrzxp*$1^FOK`Cg^7TUB4uqcfcmGjlV0nQniZykp(vFUU@HDv%A)!UFWf?-E~>o znN`s(jPHOsZQsB25`8jQF)XjFEpT306|1D_!fe;;p2ik-`)^Wqn3%nSD4<~YP#_SP zXAHzBa@~{h(gqz_VDpS&R!hf`jW)a6EZTK`RJFsz)RdQ;ZP!fs|MZ2*di8CiC+3l|D0z0QK0xB0l^)rWLwfjyyDx`ZD* zx!!STBTnxLi+Yvn=-6UHglEn@hvukweH1R1`e)T-&5`X0n?e8^@kYBo+!|_OS8=$q z8zv@+d7e?EY~vmQ7Lb#ZIh=$wuCcmqAz(%|$yE}>(ZFc)C z+U=wIIp*yb6XR1j`TWHO99NhKl!kb;Be|!rYKQe)nRxp>`3Se`ZB(?`{bA9r6ZLj6 z@1&R{=G|XequK$pYH1$8OGGwF|@qZ%l+obo^~^)ee{(9=8H=oXlAJlWNyQ;FeM`bI)S= z99>&S?QXZRqn}~S!^C7Oi13n|C_7-zjI{ltpuEpMWp{_b(}dmX_x&JW#ca*4vqifZ z^wErYn3z>T?EYc+_#K$XpmfJ4Mj2C4wd*4A5_y{c)e%3q;zmd9`dHXi_)6JfVotQM zdo;I>Y+r$ycjTeS;k~m}*#UQvk*%@N4+>n7t=SE;XjlFz)eaNZtntPj536>-vW)df z9&7ft@v7bM1nn+$ZGjVQc9SgHO}|^U!-Sp8oO%X@9tD^LBae{rD68I9?IsIsntfFJ z$8p|8o83%{b{(%$?Jyy?Xy#qx^E&TQrTeSOWs%nRc*=LE|&(~D#fMu|=8l}Y3?*#Yy4p?2-s+Wb?xS&_g4{z*pmtsP0hzy^+M zHrniBEiwkTrDGPf#&b(-|t7Osc=XRTSX@J%j9?P^=tb?v9@Ffr=_kGCI3829Q3 z%$YH44HZux-ACC08+J8ZKavw|c7a8^t9Zr}^Dr?ZOB|5;lHRB^5t!CcoVW2y7cNX! z@xVt(ATYnXA7rj@9NOD0^ya*x^e`brVd62gH@vPxdxyY0+~P_9q>^J@oLTR zgLbalMYP#-m5)*nZf?9xmPf`F;gtpOSXs2UHuHm{ zuHW$KV`29*yH?EWD<*_^q@iHfKYV-y%+Z356(ncx4(mRE+sGyDrnN~y+D^xL7j1UK zEZQ9!pxO=3($2W!5!-ZRfe8e{Z~Dg53x)?D;B*;TI;Q>P+8aOG>?T>Xd-yKZZnBuL zDhLB`cz148?SPqgJfr1E>-*|afM-b%)jfwF^nKoO-bI_;OpA8!(0nZB&C1e_c}MX6 zY*>Z|%=2yLU7K;QWcc$`yV(ia{dI$*b_*@+uD(RsElOa=Uh&`qIu3AgV<LC5qslyQHcBMaP7%H5kt@6WvF zvNgN?7VWNnRkb^ir5#Zet77#j)ee|T0RlV{JtIQx4kl-71=0-`%_}MzLrv=_7-nhs=Nx`3AI*x3#+2y>Fxc0w)x`d@#n9vcI-Hhw) zQ%~r~0^^OdKAD>_d5UV6OW=00DsGq0zP5IKxD{=7#Vp#*+M%nWxR{u-3RXqle^fhQ z8borgkf)$2Q^F5PUnX#I`9e!R8L zbNNn53PpI&hsF3+1fDDV>}yT@;M7l!+SReJn>Ae7VPaCpuzQDFAV~%Y%%uQ@T6mQG zSLv##D{wQ}$*Rhg#p%)6nq3o%b_c>jElkkP>=o&)RJ)r57ASR6$(;HmZ|t+G9k9`E zv}-N?9Ts+7!V&TE zRa{raLt@dNjnD}nnJIW5& z#CLd<&lV`VegbcnEYE>ie$eAF$2A*mb|Wm>&3-|(!^Dg%d6a@hRlAV_cWL;*|3>!w z4%H5Luz2GGw@CN&V~*OrY++Z0A&hyLuwEHi*lmp|J775}rqJ+<$h~?QWj7^(-Rpj~ zW;ffS-9cX9$2?4ocEop|(2psuF!3F+u_oO&s&;e4x|#U9-q-j+YPzF#(PsCFMZ1U2 zsCJkz=g9If?_LUv+<CqFt5xsvRcG zJE8#QUBPBL?|>cV0qOJn9Fy{ zW~mAJ<95{!_>5?GtF*;`e3+wlr!4Fqx=Goc787ztLOk`BkFIlz^0gdr)y{TXtR4r{;3!H8GpHIhY8UWTm6Z5My7q+QMKzTFm0i@ z5s+;pqsS!H4!E5}0sWQskzzvUOxg0&Z|yWV?TPR<1(>!x+z9Z$`NGmq z;4YH;?EAPMc;7f`7j1Sg%RlvEKQD$^?I~hHY|M#~*m%ZMI`4p`ePEjT`pnO@b>0D+ zmZ^tbGi1?bH(UOx7yG$2yiJ)SCb6VE8ECg-=7hAn)~R;DlGf9`LUQk!?ZdCn02}S( zW45KdTo?K4|8Krg@`?Oq7yH?GR#(MhF=^cK;OBUiIj;{-t2tZQ0ZVJ6%qzJ+Ct&xA~#eQC_r`mlXCerso&tyf14Ne;pHk}9No@`0Eo3xSe!n~>- za2LrgPHiP^q53*%_pSV87yBt#QrT@66LJBZddBp4K5Zr&T-*cZu7@>ChU}%!lpSyb z$yB^3-?QrM+6FJ$><-F5^)${}R~ z&F(6jc5PHUOk`C5_jLR28LAyHwTtHSDaFFf?9~F7k@)WK0rG{sx{lgKn_WGNc3(fM z+F`<8fmh*NaXgFyfI0PqHM7GScG|1+uD-zM#NQQt%MW_FK0l5&yQUWH#?4plFd@6h zbG&J*2d7OY?2CIh3w-AGnfm;LM#AY0RXgC-vS!~L?gxkaInKMgEbK0QOWEBmCd77( z?0dftO-78{ZDvgG`JS?q!c&-5ETtQ}*_^xXeWj9q|lfPTg!f|Ay&F&qGc4H|3 zh=xU!V}ixJPsPNX zdZcJ9?KFp>xWarWg1S#ROs3qmUFRLvO(Y8F_NgCiaBUe7ZFXN+wA-*zwfj;`*k?Iy zi-u|U{I1#o-?rq<|J8jSXrkHy8|@Z;k+H9_X;vO)$&p~N*^%n27Cv;U*6?lN;K1)>dgE_7>v(aW(-=f{j>+Ny0+1+f>ZrwuF4ighW5d}PO zscP3uU^26`U}ibzs-@Zi=aqT)OMO2m?3%y3+rqB-8_Et7Rt2Xv70mFcS50n_=0`wH@R<-*MDX9AlBeRsOPjE*+DwHEEhg!L$xFz?Wh zy`tD1svR(|-T3@59_8BbW%O4G+EpLv=ux80Zl^`N{9&~WCPYvCa$-E!ShWM@Mxb{} zbv_!I(JX8-_npA$awhw5gdhBy-%-0?EbP{Y6>gZoj2i*+cO~~IyI%#S^9?6PJc@k3 zlJWyGz8no80w0dnhRw;M&tD z+U!bNv}^W-YKI9C0Kc4P*KvIo_pT6_k3`S_k2mk#>Zo?WM!VTzV5wV zVBUaow=IL7Az!1x-vOsfrefsMq@apxd>3tY%`DnAyGymhgn7qa!MuAW%xd2vFu6~v zW$-_B;Y5 z9q?SaW9-+}587^b)UKO_UEZ+5?GZ6CH&L7`emp3o;~p@%0L~SXgG#QjS=j-1llzL< z3;f{x@NCU)fJM7eSL?_=m8Bg?I^w(jvs61^Zr+VY`Q!!F?&$>Wa=Z3}8f9Vk!Yj(| z1u?x2#usn6Q;W z5tlM4Ou7RnH2FxG7&fYSPv8M^uz3GPKbTN2TeJJjqTN?`wwQ+rTNy*kviK~Q4((Eb zWp~pD7P#ZwjdfW7r^~X~(pg&HbavD(+U&lvX!k;O)eaNp9ltD#A*)rpbplf~OUZ}) zQNzo^5DM5l|LC0Rs9m(#eP_|`lANmD_gUJR&)b%lSDZoa%Y0fU? zR|~zbo>Y2=6X#w?*!gi>9{cRRVMQO@1NBiabg}OxIDHCmhCIg zDZBgvvut6%wq{q#qFs|@)eaML;%2)TPJj^iN()T)8)7_J(HTd= zifG_dvMk2k;0HrbI%-$l!frV2pFLY#wF9mrH!7E3C;jkOIF9US3%e0LmE9OIAx426YxX{0=N+)PK<(zrtw7%H z$`07Dd(yQRS+v=`X3;KJpxV8jr5&q+ThI=#t9HQLsBjC)KKt-}svYoL$&%H4&kxSI zcKdqI!fwrV$_^7o7Ow)kcHt}4_XXyTmTq72M;UoLD?8vO5~FNu>j$I0bDVe4X1CO$ z-Ay!qig}-BX=ft$0_SvOf%)JOcV*PgX4F&dfX(+mdX#n4F52wYS+vWwRkd3$CPYvC zlK))wf@%jWPUe3d=XZpu6yQ_h@8*}3x>?tqEZXe8w`e!wylS^gOjxr-Q0x`IY*X!k zWed?86>i_JoT2j$*ptpdrAtWX?+nLzci6)2rm@QINCG=X_S>-92$){8_&d4ZPT5&k z*&R(__gvm=&F&wYb|lzi9ww{^JPO*a<_b-60RnU1L$wjzAj+>*?am3@Mn-mI0Y4}) z$x*xfRsK5<0Q$voWrqpOV930?7dII9fT<&+W0g$F*jHNc36i2|t*5ceZ9% z+M-?46{;O3)It$KF|sT84yWV-1m?4crt|ltVyaykfqTe4JG!MGY;k=pm}Fr$lFNyh zhY2HV=3Q1|+ZWh$B*oc$7k;A**yNzvA8{PnXtQf*(JuF!svRaoPy8~n@6OTeC$KCO zt(cMIY|4cT;a9;psSc1y+3@AZbX~L* zxSOntBGTC?_nnU7MVsBd7VW+u2^;e;F>wHIk2-{BM&MY|mpb!!nU}w&YS%&Fxe`NV zR`i38uR3b?sD)kb@XZ@0_!Dzx+!JPwf!QtSo6g(v5xtaM4}m+%ZgEb!;62qRTeEw{ zqTR^wMII*j6PzxmvPEH+6yRx6t3?Mv&W!zft9Ang9xG?YeILpB3&m^69 zW5k3!4(L@1fs8xx31q7yUfxUt^ zZ`=DTJK(8O=T>#2AB;QdII_`ZSK6Z8kmagf88Km1pdazw^V3y3V6?*0xCD+)~!;nl^s0 zrab}hxkoXJ?T8^bU70`q-gzE8rsA3IXn0ULJPcR7x1wAtNj(Jsfcs$GXH?T7@> z?n|yl;~p^QSvnih)1Ym5V*$Ke3TLaxi@cu6j@m_=-J=%mRuoe0dWeZx6?l{)M^rmt z=`x#20VDn{=XRZUz&T~F_jm|c(xex#)=7htjVLiTvT(jz|-VR#tRjq&!4j@Q^C*3 zR+dXj@J1YToOltZ_qs*BCy-n1GA z^@At(If@r;b{|-@YqV0e!-Sod-2(02+o#%nD6r|KfybK?e%%GwXjk`bNA1!r?3N!@ zc9|U_l^rk<9wRF!jErVKsz1&U_<+Qpsc-wiWY;&y(Pp>7qTOJErI?3_snjBN zU&>)Et}r)w(mZ3CM0%N(!yAjQ#d@+tc%>ilgOu+bM>g8*c3ZUbE>-O?F(+E0<5x&1 z$Gsl}X5Mu?SWD|>FMCMm9q?N5D8tIh+bGvn5p8xyE!r&~sM;M96Ff1spJ-R8glY$j zcFa3!@U9-D+5xwb80w=Pe(+US$9Z?o!miP6%I>_Fm{oyCY01TM+ymwY*?i5a_E=>H zY}nO%CtI^CaP`GADXTaf#ym`ncK;uHZyxPq_5T0gUN6#QDutBD5Y8=R<{Z;4Q7KcB zDWsC2Och0$Dh)z}N)jrBOrdfPAsUDxDM_ZNh(rnHcRj9s_WOJO$X?&I_V15-o%KCy zo%MOIr|Z*tp6A}z-q*hNwXd5M<(Ds1yF3E3qR=;h_cNW%mm+{|Ppt*M7syvS*hQv7 z5EIz3lQHjl{;TY+5?I12?KDTfig(O6K!9h;ynAs&7;SCkIkNF)7do^XVQN1sWNJtJ zNzc^3uTt%Ri2}F-Wp>P-_9PGzhrjO;?Q8Uyo< zx2xLi%aHGsrQI5-&jL4*;JBI8em-9%TeG{xpRqcSSb~%3Z z)GpradO5U9n%d9aV!~>SK=|zJZ*|@Q(|KvliL!F{zBVmM%&O6}cQknYkzbR2;-&&)1vzy}3F40W2do5Et8$q@2r1K7# z*tkvJJX$xq?HAP!_>7!-rc1r{MNfFnySWZ_t5cQTJTV~`U{{p8<|{j3-ludQS3!Hs zOf~1(`B~WARwrAtOLu8kK()ifM(*UbAEb;X82~?$adjFi3bmg#zSfb=5I7XCXj&kQ zKJo375^r`J9oh~5LAAq#dB=)EhHThVsvR&Z3fl2%Fo&s#{zBjda-R}O*ZB`W@SJz? zX1CL!UE-)}hY5KAzCuU;#T(4I;s=50snuc3>I{*#&SPD5-T_wE-H*4KR+8uJ#{i+@CSg9hbx+f(XYCpc9;-B!I1Ut zymV!kS76>B+ino=->K|?ExTT~d5&zn*JJ7C+(ZCGwk?J79f6{e^t5ny6{miTVdPs;8(fq8$-c~&X}(np!wZQ$dwqTKL! z7^UpY*6h+8+D(2ywW}*8HbX}4Gc=U}z}zz0s)}dLyLaI3lI1!4fV`nC<*8jW2fOWW zD7#xT*|DN9@9tp0lL0W;ZjRJBU`iU=+Lh17}c(mm=Lww z7^Tq$)ee~0SU#YuIrn0nR6Ag+-GZ*3+Qpk)Z-;i1Nf0N32gJl?$XHQ2?ojQ3iSNkr zP>@`Ek!lCLLFV0<5n=R#Zxva**$r`MS9r5(_qdqYD1d$T-;=5xFs}r7Er@nyOpj>b z6_Rtm=Tm8@^|9x?i#NN`4(*C`R_$I86MMJKdROSUY6pCYd|`OU-~VPv=l+bb^K{+; zcb3d-@ms^_;hvt_#hcx04((pPRkfQcCipD+v6BsCgG&a$WM+v1nvX7&v9Yph2Rv7_ zyR=alefEr}cJXF6&!OF&^;En0Vv;pO_QoTs9WdR$Gb5<9lIE@lc(Z8Nyq@&tdB;<` zbO*a{O{OA4Ol*9IS6om>*#Yw%3vy7r?0Noo9odyx*tPTR>mF}*UpTb8bGwc#CalIp z0(eEmk5oHgMwY4tZr)3pmZ_Tro-60s_CXj8@$K^wZ+1U8wEOl?)ee)a+U+$F6fn^z zd1H|^eeofkce@1sQM}^%v@lva$I~m~&F(jcc1506?J%)X0Q0UvQJr^31m=#B%1^nU zO&?%7r2%)ZI+=^>tOftY-NXuT~WA+D*vRiJ9}1e(%PdZU8e_&<|ar{ zyqDxE{`@H=S~uKtY~#)^Z@F{lQm#0l`sEW7Zb9v>K&!Q?A24wWXA3;z$!FCwfUSCU zd}F*~4tB46s_d>76GoR`Mz?^;LjhB{AoEVYLif!CWe40@Dg>7-4x{tm_Z-`Jv%Air zUAyI~T}3frRPkL-K0OQT$O6;(hA)G&U-V0;cEGl-qC-7T?dm$%eK%3r)e{ru9Wfq} zKtVRSWB|+!vaL4yt-rDZE-UA<`Y(r3k+#{I-E9u-u4t#)VZx~Li*|3C-j%>)$#}=W zikw)j+BFxrk7Rm&=p9BImwIX!Z+3S%w0mWMYKIAP4((V`+V{|T*I8h)WW1tfz5CGI z3IN+LmbO0Qsa?F;J?PM`T`tuQldOB~wx3nIJ^~ZHXMSm8g*;^7`!m2+yLZw&wR^(B z?mLQ}5&5=PVMi%&diI3OZ8Afy0dyeeu4tDKnPn8I!iHY@D*1MYflpQer z$fn5GUb)FTVqP->=a-TFwt5(i@a^atZ+7oGw0o66E)ihD$l86j?whLJ0)fflu~$%S z^rY$12Rv4^`?Yi!b(!cnvMU|zivFVPFo7L$3M1REqOw~hFgM72S%}-Hm(9Ebww0;X zZ_d{2HaWCwUs|=>EGBlnBfcALUNZudCF8z_clmG5QlAC3+I{-0r*`pXx67g3j*C>g zAH@WZ;5LfPcA;WA?|}PvAE)b`)Z%44Vmi$MZx*i@a8(#J`Nvbcc(Xg=(5~pCs@+jB zvF8f3n{Ijt0<+J4&@@z^t!R2g0}quvRE6`Thv0jj+MPY?p!aHPr5BVpONxx226&~% z>d*q?7Bt1DAw*kK&;xu@=-v8fO4MR+_NJFl{?>E-blk7vv&Y1Ka z-z&QkS=e3ht>@6jn_WeRc31Hc@I+84Q#%_SKmWRF2h57sWOr>%lbteE$CU-1CL_DC zWEg$?f~R)%9PD13pzP|433HD1n0Z%py|M%5OBUo9>00%yiQO9r{E3`sYj>5GpuRUg z@n(0ML%W^rRlDY5V)rugvwh!I?SSdxV!!vgy|#Kq3xThc*uB^tVN|=f=g7vJU1x`O zubE0ROt>v$-l5$ZreiYbA~4^^FO86p4!EmT_1;bSD8vO zOzce~KD(JGn8~28z*Le^(L=3Pt{kcza45Oimp%xi`Z+wc8|q+prK$eJ#O`E_Z2MZu zZkWJ3OFgBTV@?*uK38_YmR*B}*_z#0hjvp;^(Q9eW~uG{xy-P74frT`G8iZDf_~@M zlN!qm8R7Jqr&T-Pg5nhyJSrVsKlRjZnuA>jw);fzhM2UiwM1hS>UxvkDLY`^&(QIU zDCfK_$`07}?BC}5<}Kdr7C5x~-c)Qa6cfCHD2j^h{w7BTEKn+EM#+L^^c<(!0S}fh zNdL1sC3>%b=g6*du$y9P7d{dbyQ1*ro(CyIN(R8(_Rw#Z(7os=We2=KZt_mURiL< zSSdrb0}dq$*!i%0XQQg8cEug+UcXD(VZz8V@9^0}=61V;z?6M3?_^rmdD*-`23{+t zo}cQ6(Y0@8Yj%|!+FjjOwW};9j4ZhTqJXug;{q@~%gd;eI|ilQU}{r=t#;eHd1_bR z!S1dMW!FGV?2QFGS#XceJ77k3$KSfOq&iQ7+41!g-4USZC& z!v&rzE6Rw^!|0~Gp4!Em-8hGKQ%y%LOqh54azmTDg3h}a1?J0)tSH>Q|D8|g9kA8z zy-l9lz2RWj$#lWPgvi~VdN!Hr#p#*ss1GE|Q@ED01GX=t_SenU>=rt-`yr36D43ud zd3PeHU1nq#2~6HNv#KKP%gw4C@MhU7&X<=_&y4ic?jr}gsjHP8CUz%dy&KY6=iO?7 zsrletJ{@M#O_m3^v#{GfE{tw$ldai(>Cmnihp$9{i9OG9>M6kXn+(1Zm?(f(7J3Cb zS4S53xa<}8cL<|WwLG=k?O^xAYswB2JMUmuX{xf@BQQC4?kmU}w`rs7fGxXjgR(Wd z-yPa@TCCa~%hZnD44-Y5U$q0~9RsHx>a@~e(X&IMe0Mzu?b z3GQe1j%WG!##BU||UuOK&;?1s~L%Z?S zR69)YT6^yKuA^$#Utqpdy5__|?P~C|se}UVEH@V2FASsNzVTsdQnvzzYF?kmbC62S~Hu{knUl;wL=J7Bb9WTg+@ipJ$sJ7D_)q|}R^ z+AVUhYk6GREf$lk*??2#i=e=~QssRLvHOG@!7VQgwdNvvo*Ul4((p7soG&; z*E@VR=R6%*VBT2pYD>H#eMm>uZmqx*B*NP=H;iV!>#5xq2fIsf&P0Go));D}dCLe) z9KflJ6YX(x?P zx6s6VkG`tFeI!TLZ%Y`R=X>`aZ*~nG+I?wWr$k~xd`HDGnUq{dRXbo_mh-JwGI;Y$ zN7qIIkCN4Rp?t6ORAo<}jW@eiF6}1iyu-w1QqZpMa@7u)dKs!JBtS@iBHzXHS0?MtOX{M;~u?{Tp=Fd1^Pq z!EQ5W!$dGsOc+@h!fy95Wd}^?E>BeS*faXDvIDm4{t2@+yTuOenqQ#WEy>i*1}TR> zRPBICep2-DT(1M^DUYajz=3SE^9P4fh1Wc_TjOAtf1R>hD<&|rOUmyJlpQdQ-6>q) zLl##~Q+6KP->wF9;#$GLo) zrpBAyaffzeSvnF@ob!LQBm8{8e2n3Qz=WUX3GJBly6>oVz|Ex)zix9WHu}(W-sQaR zzoS#oHWw;8Ozg<=A&W=u)sf95FxT5$lrgeDRabVvmR*;>vNgMc4(+y;Qtd7i6INq_ zJXVzBWF30x118PG zULpTC?NlorS>Q&(?)jG_t?k%E6Cwa&JkpCrzE*a?j4bnxhe!3l)sY3BEQhkP zUxd*YJ3QxIyxFyOXxHWy)$VpN;faNfpl;$#O)>zc6bi4Pk;XTzRXgA&61ne|$i3`x zPwl!n*iEE2RU)`2lN~Dxdqt`Hl^rley|(D%#!|`-*s|+>GF!77;Lz^!k5#*e!~~Dv zOh$}S+>~nrQ-VidBMR}uuT(qW;u0J89~efRF7wnb-t0y?v@2v%vrmeNT~W~PG%12) z08EV1ChuWd;ikT7Qe?pPBOoO&@YF8e>?Sz0o77(C-AiJEzcTOe*$PEeJ76Aw*eIZ_ zX)iNT;IXozw4W!R75d3jyLhvk>Co}edw(7HQ z2|QS|lP?TLf2DY87jJe;9NK+jE*akw6MU9=M|{_+w`vE>Rh0d3&o$*$JK$atqbx2| zA^PhkPwnE(ZmmPRNv2WS$6`WkXMJ|=8r2S%^DKJ>>s_7?RJ(Oqw0l0Er*>bv*qNj| zCN?vRSEQL01(?eouHH!;cfCN_Z4-E;B#!4ClTVba%hv4nJG8sQT=rmMwc||IzJ`u0 zFm2rU@HVY^KHa9;{UmTNSy75D3Zw9OPwnE(?u0|TZ{8P?5&cb#hYr@#kg-u2lp9|Zl(Q@ePx%Tc6H>+{pt*YI50#o?O zMHz(wMQ%{-fG5al`>$ui=(Y8p+Fj;gxAj|Phl$Pdz;5&(%IM*#TR2 zUC+zb>`FVdD@gCBL{LUdY;25n?Qc@;fNhpXPTlE8URUja?K9B7d_T-s&B1QMQ_2pL ztXZDD)c7QW>H<@|Nc71Q?j}W*9qWy-Qdt}Yg5$@6Cwbc<=J2^ zw;K!04Jg`i16uMH)ed;FWM-E)4x{g@cxrdMgI#M=yod=SE0aslv)4YOBilw`Vq-G1 zL;?4mr|f{wmy+I#FAt-4ugTWz?r~^0!4%EjD<(t%JpUkqib|+H{*6|J=#y38ik z4%ljU#5eaDZ*~tkv@2-Jp)sL-Ad`zIpy89M-NOPC1(=;IUwW&%R6F3p@^t32YGL&H zFwc1xZ+1^Qw7YDSt|*w`uSF~FRG$;NGUl@NGg4y+5wNPGO*E*sVUJv zzD+e>a_uFUq7XpZlP8RN}vzeBsPnf{63Co!?>9Xr_-tyDW;`_UdAW36O0_N}bE z+`(=dL2x3#gd0#+6f$HlaP^o}n7f|LRuwCn4pnxAvamaKpXbQNn_U@)cAcB6c9__G zmMGv?t}&9qwE{D;+|SBC*E!W+wFBNRO@zN%ni6$665KJCsjM(lXADcTRy`u=qk^77jJfL9NHCsUA4PIOz>H>JK1VPz24JRJ7A)K zjT>%RS^B&j8E=^TS>U4*1$6u?C7R}2;TCUp_d2xu@i*1(zD(`NlJTK~??P=t0xWXs z{VY4#*|R2b48A~;0q@H9j=K0>r^KAz!w&U2uTu3MF*50?jPv%ZOLgV}lbf}V^7}1S zuLZW^O>O4swNE+Nb)gqZB6wO%tY6IeeR#b(w<$YdZY+3M&NI-jLS+Y>DpAVbmSI%l z_iW8>qC>mgom9I?Vqzma>ScB`Qtg0SE}yEI6!MI(T&vmvACmQWU`J`U;QN_~c(Z%U zp!W z?u{I(-7+z;x7)OtYrIUg1E$u9>P5~Wdq(TLTQ2bPqFs4;2HL!&r*`pXx6Yy6?k`lk z^<&1zEAgUghY7JKzx}(*+nGq`9WdH` z{zmDQ+zmWq-hUhvxViZ375&0!Y8OxK{&KK;V}-KA#GYu0@PZM_4pvd#L3Vc$cfLyi1Xozp4w0N`2U73~izkjYdCSh^93h4oAPvZ z?fSXKxCQV`Df;+!lnkwJM}tBRdegtxp~b`&S2FSbF_!{`1#Z`^js_|GdB;B0p#^R& zvHN!w!l>c-o)a(L?5=fa*Olf^i2xJ3(efpR*V#joL0N$bQs{^;-M&(1o9iB6t6idr zr*<_Q?DpWDi2xIpVtzSTY@4R+fV(_70;2q!@=&FuF9^hnApvN;J5{6 zwPXOy$dY#Fx~Jol$`1HSNs>MHdrI`Z@3;Hn&F(&jc6%;W?J%)kL6U4~eN9$$7nr{3 z9B2tr&Ypcx#RI=mva<^|r9=aK`z^$r-Xjk6x~8dmnAi{x^@h=SCK(JAn6eN1{gfZf zSA~E>*)2*}38R+27e4W3_q0R1yGN^bqr}9fQdm zR=!OGCOO#couTX|i-{ds3K(;nb`Zeh3y;)ta*6}qFRko=?e~vI_#S-X&F*c7b|t@3 z?PiGy>oJE$Mz*IZ{sg9ECiBZUsefLs+5u0I;Qiw6Vf2k}3U8T%-3;oA62WpYp;U|g zj0kV~X`Oe#G^yZwpfc~$-aVr1fR{)dkTa4VeP4L)W$|XW-l5&zDyrQlV#030$l?_b zpHl6BxfbBb8~?Q49Ls=nigq`*3!|SRPwnE(?i+`8cVDI2Z5I<8L!n)}b*ddOrKuE> ziL~iY&ewVOt-w`fqaCp!CF(!KQ@ePxJK)f6MlaRwpqSVLcaW$=9J{bTP$vIKe zQ21<{2C5yf)$ZsjPwoD4uq)Y6*_{**^7VV>m!FX-1@qX2mVlJ=M>thhkn z`r;LbKTC<0`98=i>|l2f&Y1`>Az1-C&Wy|Y>BtrlnCN)_=7bid)H3HD-~nkb}f$M(IG$hZnqNCX>fbDh9@r9n+)pW2c z{j##FB_{YR6gV@Mp@J(J0Gr3#d9)~H*bVBlz?R)beX=#XCJycHp~^E6G!+vY-*M^* zK2+_1=}F2XT2|w`A64yu_ep&BR3|AB^t}YV)5Y#FWrvB);IYr{ASg`+z&lGlr7;TS zth{hYJ=9#a z>mhJw(XRNdVRTn-PwnE(ZlFs$(?D>Lm=L4z+hL4+CHE!OE))Kc!i9=EOJy8=OSJ>8 zDY<|bDocZzd7j$Eo82gfb~7jGyn9AWcsj#g!D^h5Q0;)p1w1sqCM!zD{sF2Ta4(s6 z&)yP72YY&IH`&3i)ceZrWiesBiT_N9c z@_4hG<E}9IOl*FUk)3m;Y6r|CT1xTREABVBS>WZ8oBjFwl&Et-&ykHcyX6k; zy8Wc{?tL+_IVi5mURa{q0duZc^x6@5dNd}j<-O_^zy&4xe7Im3&GLPa7jJf-IJ7HO zPPO|qQ#)2;9`~e@bW8@oTp)MeUx|mHsclR%99;;Nqg)hgHLcU8N6TI4pmu4;EGi*~Jid(K}_<=mNy1D7c~OkhTgLZ)KjbIJ}FcC5zYFlmh& zE4vE?9x7gOL0O4We2YHf&8~<;yZh>>c9_@<8Rv?j2UNQ&1g4Cv$BKe_e_Ya(;sXZ~ z-+d){<0c(Ey`r3h-P>GDBmztrSz|XNH@iC>+8v;GK_Y0Esh#zT zPG%5VcEBqn%k$;kVRZF-p4!EmT@ROb$5cB^tXHt!EyyEqG61#> zQz>73c&}>LQ($|yT_C@wcJXF6$f4ca->G(wiiyqN5q*}RQ)w~)#%KBd98IB4A5!fe z6WD%HecpCY?Vfe8yVu-AVM1JtSHLbm$J}H9j92gh5L$mv%BSp}6S$JxS6sL;C7R(I z8^@d7D-P{`YN#WN2_tJ)<84h;J76Lx3Zkjg%C$?ido_!89gcgB>>LNXGUjd@6MWXP z+ihYL;BIy9(wivQ<=L(5-VxZ|^^6{qt=YZr(C*%%Im9&_z13!1yd9 zOL{Nk0oCq9f!j)aSMfG!g6G>R?NbN4GFz41XJV4|7VoA9l^yW+GB_?#x-8-+Tc6G#r(>CARWJNhv zRkZ^q7eH%BV&kReaWrsqX{-6wdtvm9@26dEao`Y4p_Lx?q`VtBy3LaYW7*+$?|5a zk2LCiZn39!Jsj-b;aHsrdSO~P8!Yhm65qX_7DkO9@EqBAvwPN|UHS2< z-E(4sZg>SZ0y*ZYcEChX^KUGn56hpJqS`$#aOY}LEz>0>`quZ6YP{LK;?QnRx@z~T znAltZd6eF{R6Ag*w0Ql2tRpt4cEANCa)0GmO0;Q>=e(QaVAo@VvU^8N*ee)a*1PIv zy#wYJkG4yk7>6EHcEFb1m=W2U-3Jcse%YhieJCb2g5nl$=L#KJU^!2u@pM}=WF~?F zo*=RDsAL%Ryx3E_&m8RLR8V%Gi;1l?qLwE=SC7d6nD#QvJJ{taq9Y4zUmq9xBU`ik z&Y@lTq-wWAOz;TySt9qGfocaVdqo;i0H^II_NaEi-Q-4Kz=L75H^WoAUmWay>8I>| z6%*zi?W(_%stU~`{) z-%#y<$$ir1=pd7-WA%W z+5vN}pwdWAj2Rm%sdg6&Y_&Vx#&h1qn_W?dc0cni+(dwht(HN%hksD*t`wL&3MWRQ z&+i7ScEF!V^m$kPFnY=NrC^GKUAbG89VW1|Q9vED-h~2l>Ur|l$}44MNq^nc-zhBY zPBaLkD||C#@n%=Yq227xsvRciMlOKUcH_HsWPzm~KtBmC+erFNTXkep1>Pzr#-YQ* z=+yz9UU9R7-OukTJ4|55enIs4!k^0S7J+%8$_oaffI81AJK!nO*659mVf3qsp$An6MJt zOhwo0l^rm5jN=ZU(U0R*yh_;tFBhL3+%SxueIr}5d(5F-IhNf-Fj!2`4gH9q#-FF! z0izwgAjB)u-hWKB10F2#U6Wp6bj0_yTfEsl@6fJ$SJe&^A}I7@CtI;bwKK5EkV(IQ zw4N!d-Dt5cCEES^NEjXWty+jTyD1Ls4&I{LVZutxykj-)YxauQ1m-OsH5GiCd4uVq z0c@MiR{O`(XXiTDl}%H2n81#mj9kEE7Mf%L%o|WnjI4JLn9q~U6Zm<#5$IPYj0)$? z*6da|w3~IQYKMv4XUUKymZ)~XRy(TjKQr2;XVLD!CQt1)IM^NhUfE$ne234{uc~H3 z*#XndfRUxgN16N7XEzGGUG6Kso)JbD`p&y}v-{qmU3XLAh6y7}E`arJEQKJ+0GP;~ z+^2pr@-NkHr@*1ackfS@>cFy|BYVifZq^7LSxoF|Ow~f`>dFonc74;zE4v})J=tM_ zrwO|*&xFyY3$r!5GY;*_&Qt9$VI{V?fFb5g1}v2_TK9>=oXD#q`;Wjj%QMUO18?zW zcTx3o=K|*ESM4qq6ZQ%l1+=?B=UpCw<&|LFCc95*kB_I#=Y@eUmgqC}&M?}z!gJom zo86TT?H(XRPXw6Q9F%4kkE?c92}~Y^D1iLkF4Lb6*lKsZZ-11~!ESz}>@dMASo2`_ z_jYAhL0~$4@w%5R&vQ?oEfC}kfNiFtr8Ky_BFJ@)xl_L)|Cr;y|MmMlXNw0pf|%1w zb*Oh;Z&fcXQ$2g$USq0efd$gB6_-iRKS$RT;8cl@R}KiHf)9C$cZ-AF1JjjVGcjT2 zF}B2?lPN?_2Ea1o(i$)QL*IXlG8HMn+lAd#o#nk-+icCQgG0ODo>%QUiiwTz@QVqd zY6py8j4zy1E1?Fyts~n>-~poD9lhmS2fmf=y&UYW`$yUJ786F6c}Eti=tAvo2+YX> zua$Y1diLxSO7DRz^hz}M9NL)E8{$%Lv#N&)Q2-|&qW6X@g~{M?fn_&KV?{>2ttKA; zoGMweR^Lgd-d3LC#hcw|hjtZB{u2{>&qxH?yNzlG+%^B?x_jR&s_)*{n|Giu2)wWC zz(!kgSBN$}?WtY7*}dk_?p>32#{{qCY{7|p-sh^_RDsDI@A&&)jiP$aQ>O(kEYW+X zSHq~vPoCP%bFh2R+*x44dcrTK#y#e2F<;=5t!C)wtf+`SV(uA%=gNwA^TuNQwK}rEVCHE(Vm{5^neA@B0P#e7o4y3@(FC6_U>t( z6EEKEiaE5K`>ASowU`jR^UHF)Ag^i%O!LKCYJ8eOdiK(KsvWS^Zi;UKW4zg2=g_X# za@DS)m=K^a_i&2>rpyhPC%$rFwo-!R^e;@B4ET(A#U-!HwLpK*c^7YXbsgFrnyA{< z6BCvbd=~BIQ+1gPfVpU7qourZ>K!`ofX7O3eDsnE(VfLTwY$~9ZZ03yN(8rw31NU` z_Y?g;k_zJ$L+ZUpIw0eZ7RnCmxpM3o@wqgscrIJByUV5B0M!l?bmNze_EI{iB?Dm5 zOs|k-v8gj;yJ`nKK)fQ~!Z2Ff(Nnv4vwOgy-68JT6TyRG!b)tfRJP7l?SLt)py-(3 zc+~r<9dJ#VcRS>`oyRwXiZ{E*9oqFOtJ*ywCRvY+jaI96z%-blpobOZP)*eic=NRb z8{HmOh}yjDIq%}l?gfW-y(g=7W5k4Aj8qS+@o*}olL0V|$!XD7a>t;w`{%26z*f6E zzVp;B-t4A2v^%_1wR>Gm*vZh3)bV$wH~^Sqk9i(FCN1wq)oz-=LnT!)Wpo(*_PVEb z@n-j~L%VrvRXa>r@9ez$@U+f5V9G0*clhk5IaRv_0*{hpz$q!L*p>9uZl!}=1DI()H? z?2lR4r96?X*&T6cSK(3B4wJ0wUEQx$yQ2b=NFjk&Ye|!gb`w-P;JLEieg1S9UFKUj zd-kk@-n`aIPi-tePP8TNP<}y8)0&)9nS@vAGb$@T;K}j`B?%vCTOZ&3;{UPx?-lF+rfO!0V`P_Irz88gnAkXlPl2sIqazE< zh*AK`Ro;naIS@ql)6zIKta@p z52<#*R=cjgPX=Feu&cg6*-aG_8{_dv<@v(O4wwuh&p>38OMBRST<~>)Z;=O$dGp98 zN^*OSY`odcb7=P`FL@Hdd@;$I|2*}TY6r|UsQIYnnDpG{*&DDO*_~rOwTm~qbcc2i zA5rZx#DwUHRfPzu50x~@02u9Nt@}KKrwj{6s8_5M*cQ*8IqIohyxDDZXji?sYWIbh z*w~#DcW%?52bf2BnX7U7M)N5!V5?pITRgSf>0q~%!)qe=K}_t5LKKjf7Y<2pyV z5qExhYyEcw2KpXhy4=8v37)~|&iQ?Kz2D}meE9^Xe3pC)C!aaXR6bz)nUW8DPmRSK z?5fpJc9<}_tSqc5=L08$s|DuqHU+9Y_5F6IvI9OUb8o}rVN}Pr zu4)HN1j@%Oc+RjrL+2fERhf5lCWTRZ-=gw(v%A%yUA4DWyW7Ns6@{|}g~y9bZ3QqN z7^Eo~^KO#K;sGz0^|);}`FM|SbEr-ZcJDo-?C#2B$9_SMajhu;1tv>Ij!~Av)NQ}% z$aWUkHg~^dtEX4Qo81Er?H)R*+C3;HtSD$lwbs(@>J`9L%~1Iv+NIYul^=ZsJ}GU# z`fZg@<=)__-4iZ$q_GnLCN{<+dfzln*#VQ?mh-Ic6)%}mx1j=;m3+Xc+vOpMZ_#$V z*^O~%H{c%C4ikH7r1E2nsr(o#Ft;k1&7oFAFR51mTkXa-_8i&Q9qf+Ztn4r$0W+c8%3kqF-7wnS%X3AEH@hPa z?UsyG?J%)EOLq1c-@8l(M+N55AU7(U&5CtVp9Qws6`SX&-PyAadR6BrJ+-lM0P*Kb zPb$Blw#nl0jGM8&`)}n3TvP7y{#_nM*S5~){PM{kd#;~j^ngeN`NhOOwje(4G+osL zruqZ*q#{49im8bzAh3Nl*zTmKdRIHx^{b}rii?SjQYXMMXI#hhLrhkA=Psd|`LuSLCQPpNu+1?I*K z^|)8*TT#~(;0|&!8__3>zVdA#Fx0_rvAMaxB+32duC z_xLt+i8s5k4(+P6(2>Q2gCW0+>=tulHcsH?qbH@A=B~*V1Ba+~!1d*1cIiZ^CcDjZ zJ)Y)Z_lLP{#Kg`!;(%^^!z3BJAuzX%>}7au+t-yHuw|FXm#x_?aA?=x+=VU_6Jh|) zKGe-TVJc{W#T(MMG`mL5jU(y==FAOTQ+&4ZLt*q}O;7DsIoMS(^$Q<~i9KC#sx8Q4 z$7BF}{@f|zv%51G*^A9*8Gv&NyK4uB(G9*IE{!+4%?|AroB9Pz*vs%)yyDi9s@<0Y zvl_2CaZp^E6O%HG$sRJS}}4eqkX`7 zQ)3A{Sh6X-ZVIE`H9WP8H@l$DxlY?sK>V+Wln8Q&33HCs7@s}P7gmxAqaC?z=G~1y z>Ab_*YB#ozr*`pXm*1h?QI4*Opn#Z=4X{4j&6Ius6Qhu;kSUqk{ zyxA3ZXjkbC)vkn?Sf3?Jwt1b-J78|O`JkHY85z4w3sB&~vd<156h>PDPwnE(u98E$ z1*Dr2LFG*Cn0Le|YbZ5OD$IM;51NKr@m})_op)Hbmi2C_d@%Qs!k*g2n_Yc}c1P>0 zb`8YD?z3oDfn_im0CQ_WMGw{6!ekx8Q2ku8Wvt9oZAKcu5Apynv^&LN>Xyy_c$X zT?Mvz_bKH(wd?C*XBw1ZV$T)qv#05Rm<)iKcf`hWC`$Xz)Tj0nxUj^=m+z9^2EH$R z;>~WTL%YhubYzERYG>EGM;=n`fXTY^(TKNRU7OZzjE?MZf$ir&UisB?WXHMK(PAMH zV8Xm3f+ACKQ7L5y%!!dtmPrJadbpOddr@H9p0xM3*_z!nhjx8`Q0?9j6GoL^qJWH4 z)ee~2ULq(W*sHHn?WPOdU3Rhp&B7>mCr|Aby4d}p>@cw{-tpN5S13DRajU}ASN8XeABsM@U-nAMmUjp8(QO5dW|0T-9|`5#sd zqs?_ZM>gK~WiLp%NMMH0dv~6>)ktxlpSzcVK=u*7)4)rj%>WyQQV&81U=G$_{w5 z1S!qyNsyAg*)?!zmrB)rB4{Wkj4U}s0=#c;RqcRz%2;woEkdaDZC9#xz|*8OAnlzn zy7Cdvk!|5%x8g-**HTPaiebo-vc0FW1MYp`Lw#Dt^Pb>lWe058?OmF!*>!PfH+ZjV z*Huhdih17CdH>LQ1&iu(3|w(%sSFdOv`%j!AFv=w0beCfTUsi6_x$K7Ud-wBb*Oh{ zqpH_WOl+9KQ-=Dc>;ssx=nKDzbj9l)s(Aec9w;H+Ciz-k@sgh6#hcx5hjwX?sdkvy zKH#h=TdJtnju4og_P4?HGFOsWEVNu{d1wR!^9>l@Y(JrN&)8i5yeJ4S)JWhM|N=*c4vGG z7vjxswL`n~k5xNNtaj`cs{$R_H3C!cPFn~fydkC(3OK)vY|{)Wcz?!oWWRE-OZ!#X zZ4ncEmi-K$ec`yW1AhAQZ|fGxIWdDWvd-V@$O79ZgSm5LYj%4a+Wqs2YPVNRtj}Kf z&4_v<`>1xnltR&nQ6jvI&E_%+c&qGWGv)h7?^O5HF5c{pIkX$HM78@vOzgRb!i85% zIXN(wpha?CE>XbN)HBm{-T^O{z2fkv@&$9>roHiImrOf%I^glYRXa@3j=4Ag#$ok7 zr^-AT0Lwy?d6Abd?Y;+9yPN`_lzF$iy);ktji4@du>1ELWrqnX3Tqx`#*XwZPX@qT z9~M~ghbjFm zCGahBuoyBSj0U~ssonK1cF!rhDw*uquE_H|IZ8(sxLcjO>Mk#qOB+@^{fmw)@MPJ^ zZkZcKm)@7H*)?=%_qcfi5{U^ZJp7g@V5W)hfYHvh?jKR-z@4gHBZ1EspMCu?DO{-P zsa;D4yMN6SkXB-1S7YYgIHqGV0Op4b9`kOoX&lg6;G8nDyHY8U; zcV}wHnn&dRlX>(8%z2h5K>jKH^gtb1;KHI^?esAE*Y|NxyxH}0XqRCgmGu`B);s1N z(dT}Sxyb;Sy`oB$DtdvO>x^m#JXf@Pdv+LI=NkpYo854Sb}N}+iC{z)?TDc6Gui=j zp+ci=a~V`LpyyBkO(lrBiLtIjgRs%*rdYj6$|>Eug}X1HPVrtDc0R3HXgP$ zjJo?Y!z&@5%w6nYclPXGI<(l}wEU8Uy2*6x1t!hI&=R_rIj;1U2yCw`_WtQH@nTMo z4-T(!sJF7FiiZht0DeKckMTF@*aGulkgGgCe^k7minlh4cvE}}XSX=m)w^8TVZyND zxm>?@yi?h26__uVvX_y;Ym-Nxdh_;c#WFgknI_YOJ6^q6{k9qOGuiwh*w##YTB z-$(0JzJ2olif3eA=B3`-LkAbw_G4cDlBZ|Hm>!<-heN$5nyPrm#l+4%JmXnv%#r~x z%?&7Glw(o)TJw|^*giiRve!er__IsOs&KBK_f2~!Oze)qZo8N_qRAkqz?99tUaylfLT>o^C%PCU0&G%7njeL zttwF=`fh5rW>?ao-QRDkcBRAwudrwCM@Oo5z<31>nK_wtt*_bvTkT%?!Be{`4t9e~ zOSh_-?AR{|1@1SeT42iY?b&C~W@QI#KS)>1w>&l8>>`JDwHoWlHWCwimxs^(aaczd zn9uld_TdKpx>c$j@B;DK)1QS=_47SPwv~h3``wjYYcXL(;TNC%rkt_^CI%&o$CTXu znz94#Bay%dW2AY%Zzd|ike*DX^!yn@<_hk4o zs+~10j7rS)9NBoY>+jI+Z~Dt7f&p2yBgc5ZIX4265nv~i2sG`v^He+FAH^%Cuaytw z)b-RZ-t0y=v^)8fWvM4wzfoCx5N1S-e9Rs&>FuyMjYJwHxnXH*mJH zn;<53C!^BsC36o7OpHSIn49iZ`<2~G{0GG=?pqZ`^RCI(>}EK$TTY+PL@-lKY<$Ox zk~Agd!0fXZCw|or4(ESIwR=-w8ynaD(o?&|4t6#FRCY_mgtd-ecCz1>>%0TzkqUbS zO(Gkb-1d6{x0c-Yi+yD$o1U%Nt#N2Ku$F4KR!o@oR9LdlmKv(s0n>bmDnVl8*{5`5 zfj7uLJ7z=}UHyiqc3b2>cCMe3g_PY^F(F1F#N;lo<{D)OJpaZwGKf+5FSNW#*?pab zUCGz7HM@Nd?FO1V6-@A1PCmrORS5f%!G494lKlBy%JE0k8F+ze2i!+$XA`%C(U-q@ zYIoeh?xeYo!X)d8Qg5`fJ0UQ2$A(>U#XAXQ2i#g33YYvPB}yrut=Z+QcP^ihRvxAr zDwmkp^DOJ#+VZL$FgqCuP}&@}Fb}wa=gP=l+9-@l?e^3z-s}oGv^!l&L`npgi3w2v zzi8KRoN5Pb8>Uem{li;2?|`SsUU6}EXo0ygP7*F{`y2pOI~FKu5N*z`evP?tMV= zcfJMXtzGO)o!jkVV&@%^`>hjoWPu4?c+g0UGH0H$Ya{Ugs!}fV9NBoY>*mnzLsMUQ zkBVeI)=XAAqd zC_7*(7&F@k)oo)=+YbvoRM_4AWf(2>ZBZU?c27FA``}^K4inqr5U==UscHwzSC4G_ zpx^G+k$ozQcFW%M^on@1d&!|)?Y62NCPYv~phN+OhpTqL)XXsNXf-$0JXM`2@K%|3 zHP?kv#SNa?#hcwshjxz=W+s9+Gqq#C;9T*{B-IZ1k`b#k3Xsff>Ym$m-n}L8EIBc@ z93|(9v7Xw!=VF&z*}t%sdzlR`8T=_QIVk(l z*R|$#4{&}N+4tIp(XF>;Yj(NnpF0=u;Ge1;CU^yxVMGCvl|#Gp1Sa=MTLby$v*t8cYGcB@LqASE%YISqE*F@c%)X=TZay0YTu|m+ z1(|n$`ZgqsH@njEr=IJlqPb1M1nro2L;+Khs$Cg@DZV@Ub{Xxt@Yf5v8Ut^ZdACX; zs5?q|`fN4%kDcr1U2_vvT})udc^0oIJwVw3vy+iG#w$8pq3nPyyIH<9(eY+?gZ!!I z`Z>}>L`nphuvg#}jO_e^s$FA&$&gV8CI6J(AgS5`7c4ih(W5ykL^*sLY~LaOv2*=Y z*t7R zZiETC*;7v=Q_Is`;Qrmm>1mrR&n}ZU2DaL@@a+h3oebN%F}`+Xw71nul< zJZ6<@2fTlCV&(qL4Oa$1=aml?Q0>|ZTwj_S4XqSL(|lVA$D7@K=Mqlv&6IW|0!*w| z5Pg;{tl9y~O-eo5e~Wiy3^9$Nx(j^1L;-8=ls6#0xzBjBdqn=!bNw`5t=eH?qX72V z<~OT$z~t|^iITHfTFb>c?*8iFblCm2m@Dj;t|JW^z-d~li*-ex`^;|zK?o#b0$-m;L2q``LaW;(Ll1bZUj@WqNmfC;OyJux2Wp!4pez|?AUVwAu%{Xjw04*00#+&4TPMlYs&YL`35 zxwr+viyR;l0VcLCfEDGnYn2_aeW%KDc*8x)?tFpmXOMII-dDt%T_J~dm+X|5a)|&F zRuq0&QMyY@ISyro1*g=AFo1N=`z2I7;H`4p?jXnQ`VBpY_F4zMuXvl42+E3yP4lpo zRZA#4U=}>C@d!UZFkg%Uw(NpLwq{qup3bz`ql4Wg&6HgeF~Mo=#QVuyG6Iw4p*;^Zp-W%XdDm3n*23@4ik0*wlY?fQ>KXQeu3G_I`6Nf7iA-g>%0TDPv7VGK6i^ZyMYevzG6pB1en-^ z1&QP5ku(_$5}2O-JysOd)%d7MF9IJDpRM$UG;#3_p+-5_Z82A%nAjsD?5@vHcFzb* z@{n8+H@^XD$F$_ z#pHa2?hEr(Campx+~tpK&F-W_yYX#RyT8SRy^IKt74IkdA14D~&OH>&NLZg<{dpZ( zVEf=L&tOmO;?3^-#JSS}xBsl#T_7g*%*cvz{D5i)O!~9?xC-hOwZBpA)LN2c_ekT< z8mB$Ai#NN%4(+~Pui6#Kq8%rTyNjxJz_f>CHRf_)p(zcxLg1ki2Ml~SjOu>wsa;tI zyUUg;yK-WZbw%macZ+5j@+P$jUVL~xE zt1(f)fTvVDU{+((%xz41g)h}BYG={znTnnx+r+`{>pPTPQ!&A38C~YxE#^@^F!3il z8Lj5>UZw1A5_qJweOk$e|iui7q`9|KO0w0o{>|%M^^P6we zrFgS@#-ZJWmsC4U?7Sm_N>)+ro)!3Hs~K7lO9eyP?6!WU?B<9G9%1v^H9l2# zz&ujrJj=OaZ9^Sd;QG?|b7tN!D%mJovwPp6UF&_S-3MaA9&67P_3Nv4z+^w!E97x> z+DA)NJK#oA3bnOG7@cYEsokdzb_J;cNCcmW2_tLS-7rVl0kf0YR%FFY^a)%_cCrrS zdCI3`Z+hQ4)SHk?)%#9NvU=@xRaHG;8XMskJbb_K4jo(IZW0|=SR6)Q6!H}B7Z*EI zs)b3`_4vE?$_`jg6gu-{V%91BzOwr@3%h1>vNgL?4($qZ&z=Y{u_H@#Tza}{2i!IP zWG!8wWM;m}k)0NJm6R?ty*rGK7WULG-s~<+o;w>b$&^H6f_AJalzmL6W*`{=vzJl8 z$X@pTBRcOc61b1V0sXdw(WAccXS~@JacK8VZJl>lh>1OK>zVO!)ee}QcFl>hE9G>S z9&J!5NDc7>=xO$6n|#GYu0@D`hofdShlDP)fKtWb6-0^4jrQ{VM2 z-t1~Sw7bJRvcQCqWw%AUj~~>L1;#7L2GE9ibxzf;P8RK!`Zh_q$-%CWsrtag?iI|t zw&s~CFu4kPrT#;!xd=%V*L*SmPLYv<5z(s1<(Ojz$2U2Z|I-L4}G zY_kEZcby;8k!>&VOo{NWxgm`HZsVz44+pzDOsN(oj4bnx+-wEov%uWs(ZWR*>eRIj zlwD7O56Z~ycp{8O`<9c(o86-h?Y`NmBZ~>zk-=jp>p;3Y89XNN_`)Ogwg>IHnd~R< zDtQ9(;?-gF!aC29jW@ez9olW<%La)66Fh=ny=DAWwFBl(fX@rc|4qBed=m6IfolLtjZ-$O6Feh5}3f8-NopnWdN8rg4-5}n1jztcB>m9d;~g}$pHl^1 zE7^c5zl6~RzB^gG+0Aokx9lU;4iolRMwZHer^={y^95#Psnnuf|7g=T9eA4T6&p*2 zQKN~T^Df=NuEr5%hY4N*JA8I;M`f2GaP!fHv@(@v3{@wpR{(z^>_!xm1~b0VXS~^M zbZGZTan%kJv?KdTY+PW8YWGE^cGNFQY+Sd4X>tr~wVOZ5b7bSqZl^=Lzw)YfKZprl zfqrN=>X>Q=Oe+oc3L4BDHx+`w8zeUVrd}Ae&F!gOyxARgXjkJ2)$TVju|CUAw)Gy> z4p=@(|G(xu(HrWsz^M|szdA9DLf=ns|Knh{tc9}sS4`L|SWy^R?HHL1fI0Pi&@?2z zOMmcXWe056yOF+oMZDSNx%}KQO5Yz_lg?%lpS#C^KxkIrvz8&%N|vBR|`B-a-aX)8%9O1 z$kyzxb7=Rwsp`eVdIeEHT65K|qQK-)X05wO^LHU76Z(CL{Vxzf83QmY6tg z&5470CcAyMdIj)t(eCR%Q=*yMJ++HBy9XTF^)(g84~mK1E7)f%m=hx~Hv)U#EUItH za^0-j0WX);xUDo?sQj&`c8@#Q{cdW>pAZvHi;OH@@e3PVG60t5cWE>Yki{mgj)?+* zzn7it{WN*_{#mwW_ku&aqc4j{iC~PFuvggF_zM=Aq{0#t|L+rO<;^}jHj8$fJ9uh0 z)xoaMGG+I=nAqr(){y@M$_|+I?vadL8N@k9%!zTDz`bOzm|s4OCM?RG zdsj@dzO-!lxoQW@$c~Jtgk-1Kw#T^vGi(B?J^wfDjigIE5#)1PFAb1vI8!X zb7EcT8?B!!9c60VRtbDaR+NDq!|11R*_z!ZhjvFxsdkvy*qHV1663SLWVLw*O11W& zwK}q!1+H2}J_9xT#H78o|$)aBA-6>JE*F4pWJHLGL$DZqF zaZ!~oznIu43ctPjX`Oq(v`#0NB8yIX`J+1bfXiM#uu+jOQle|`^i=O^2fHfOlwEN# zv2WgBw_>)k15Uf}4n5QI$%q`UDZ3H^x0V=h>1|c(w(Ke|&DQLiJG5IoR<*+f?d+L0x=FQbAuxL_k$}A4O5fE@wF4d? zkwE3?DbXh1_cNUx?E3Fkc9_^051$=)p|a~DFxAG~zLR0R!Bj2)*OU|Y;(TEg`PTEq zn_VA=c7JSA?fQy|9a*YA9)D7`0~T-iUv{>}$2zjWOXTEJwnP}+v(wWn;>~WTL%U<< z&SIFD5FeAVU`07X*qjW2iH~K@Nh7@0skg6E?SPBRyxVn27&Z97Q@ePx8|%=npSe*P zCnoqTdjKsIvQV`Hw%TR*W*DbA*ex=*pl^tYjZt9t(Id(Zn7RsT z19&g^p82RHaCh0s3N#C&@n3k3Y`obmbZM7IL`nph*s}%NT{Ts;1Lji^wogiv!8)>w z1pZ#UB5^7u8r;-VyN?{~j;&U9n6TdQ%U)5wg0fpJFgqE01!vk5yObSpcVXA=f-pL; zH(Rs&(xF{Hp35czOc+@k1q`aJ+5z)=`%sIM8RS71-=NxkC9tgx_}e!&-tAzw$W$9) zVn1iaYCMVOT*+XMz|1>V6kg=D-=XY)kBZM0tP@59uJIh%|HtmX&wTauY4Bs_8ZAAAVrer5mO%B{#VwA4eh0)4mo+BG?b~y_F zx9*^2ld4@pOjzseP2S~aR6Ag@v#fW-DDVEGUIA>in={N)yLhuJ;LvUsn?fSEL`<^Y zMzuYx+5z+8?NE!tTIE*BR96C@locgcZF%|bTbUYfb|oCzbvJLyt`QS^Cc|gzKC0RQ zQ!&FWsGKj-o@=A?4%mJgr|=%nc~{xN?x1-sc)gg|QxEKRmQ;4YRBG`Ol)d6J^WqJ7 znvCphdBrf%_f{a@>>4<E=c^%IkbwJ{UX~0QC&nG7qgH=`w@Zw2&y``+bAqRK@n$#7q22w1RlDJ0k~KzIYO0NZ#g#v# zM>HQ7>}7ffjS%=^Ig^dNOzJAOcxv~egI&4S$_^9Dj=f^&RvlSju6vdj%f-m1f4)pt zl<@*LlG~mJhf<-Zt{jQF zM1YCiXZ1`*N;Ii388S}WR96mZuiC8<>jB~w2luB$1$TIA_mzX)>|c}}Ce~-UiF($2 z)N+f!tj2uWS~j`#m)}!%z)fUDc`1+7FOJXF?DjacyZ=?yZm*cw8&I^Xl%^vKOvMa+ z-MOC~dPcPao+7bvrN&`Y-M7ziyxARdX!rAbs@)%ALZ$-GAon?+=TgZ4n0ZJ48{WN_ zH=nZt9xU4Rx+#pldcbqu#hYEC$hk8WJx;53Nijh;_6t^&hwoPHfQj6BCqOy(@fo3*UINtK!n`8PyIG ze3nnDQwr4(I2iyl@1%Q+WO?$XAMdEzRTa3K?6bS&Hf5RbXVfAGyB>{|9VVht zwVjk5utc631@MZY@N{L@NZ?sAvad7>qk1Dey&~T1TDr9BsoJ#?6Fh=k08zjp$_0`E zFu6}E@%fz9eshc0THsWP@0uS@iQ4$y;>DX?SBG|Uu2Ajn78AUJUsjZ6LsdIq?klJ? zVmEwhl+L?uS+u+S0?&CDZ+870+Lf=W+VvNctPxb+E~*`{eOZonf17&k0Rp#`73GnO z!f04^Pwj>~*rkkAb|b_DuV6)Cy<7jQvKuL|xdAP#8M0~Rb!36ZN``F4{b6)xg>21k zyh}SuC=&rDtj0FJJ8f?9CJ4;UyUnAVpK`W<)ZYQ0kqp^%c~eFu?G-^T#{EBj{%d?*<_}a>&v>7g8z~e1+Xn_aIg7@vdl&IG6Y|U=5L%W`T zs&<&*v^MR)va1RHI-GAxG z0_T(zS&oG%(GuShx4jN_J?~X^`^1Fh_;}`nydndY9Wa-R6r?ckE^egkfcwbEPFR}~ zo!IO-vhim3heNwxo2Yij#l-q7TUmM^)ee|)Xww95O!`yXRXgCx)dn_NerHP5?sre^ z;>|93#eZxvnNWqlRlA&GVh+7%QNd=~v^8c=w+Y6mP^@BbQV6);7!mu1oJp$(qem2|M{{g$#TmC243 zg(G8^Y03_mV-F+Cg~dOnX+UX#_sNm*ozyT|?pu}`Z+2B2+8rLH+Eo=3bhA-F>KCdV zune}A%gEE2)SAXCstIfp$2EN)(Kd3iyII*`0yBO|&9?eS*#Q%Nl3rvdtMIh0cQ*h_ z*+TPiVbrmY=e&zIyVefvdeem`5!@~&wv`5Z#oH!y2c}61MPwpt`lnq~JK#{fBFF7v zbi%haWH$%9d4%YR;2tqyCu8oh8b5A|d4MTfpbZIEQO6%pcEFb1d%mBsh&Q_d4(%$K z7R(Qci9OJ=-aTfDwSjrEWzsyar}uqRwFB-fBU@aOp9O1qdPTh1jdW-iKCAQYNinhW z4zGBe>6i?FIZsI9Qy-K~H=h##?jR?|f0sB(T&KJ#CY*Z62jCT-HPDd-Caq1uG3(vl zhn3x10)HasilfU@qW-?07K}H$B@XQlnQOfF#Kh)NNE~l8#XP|56HCl| z-t4wIv*CNMrr(JWq3!JHU@n~PU$`XnW~z_;1#eh0f=^e#vQ zKV@OZxuS+C`~>b+XIC0g0MX~j8alGT%O!FA@xLk2Lf?58Z+0gf+8r_%Wq*o^jRIJW zH{;650GQk-J}X!785a*z?SRXQ&z?Av5*=9S>9g@>m#gTxa{&*Yuk#KQ);m@d*1Huw zRJ-#8rsRXzn2WN!<~$2rO6FZ#c?7z*y{C5ZW_OuGyCY>(yUWGI=I_{N3m#YPfLV?C zcnbP2zF+4Zu+{F$S3I>V?O<2&4rNy+lO3bWxnh5cvIAzllVW!*>HXqXWp}N>sd8bF zyRv*O$~V4?H@oT%?H)8wqcOoFY?kLQ(+Z-7z&!up>C9VFfR}f&Y6onu-p_3G9NESW zb`=-s$YMf#C*7aaXAck6kp-53UW>IQMoGQW`0R}WZ;)%e#a+T^lkbOT;?1s&L%Vl5 z8YTivtj}uh^Cs2q4uOe{Y4SmXl;NN0$N~=)?N;>+qb;30NA_L^y9bXbyZgihe`RD@ zQBs;IJ7Ds6Z@pS-W!?&l(ifV?DZp)o-9>%F=-E8kn%%<=?T!!xCxS=Bg!8O8rmlC> zepBs$@mceLw)Kiy=4mwWNXZ3sY%0$`x_W9CZ+1^PwENApzIj?qIM%S#5z_4tCd>N1&KwUGMgqLa12+(;AYp1@gu{%jtRtJWb-e-*ZS2*}-hh zZka>7`AHpFOzgP=uej+29a&&E#tn%y@J?H<^pBa4Y0S$uY(iSM=xEc>h6 zycbsO%9<8IRe;j`Sl?>nK|0o%6eqkJRxvJQ47mnu6#Ua_^gY6tw*t83G! zsi3kl-(@_N2P}?~O2r4UaTk;2`2g)?HGX(? zN_463Y5NlgyXj?>-KS#0PR1{L#mnXf6qq*xUk_}mpR$_QTSxXYf$dGyx|W{nUA)|ob*fU?8H z*77j21#2q1Uj&w@GxJ?fzVv5yDm!4y?t}BPHM_qZ+U;ql+F?Qjh0hY-m2IWkof24b zo*KE!x|crrMb!?txSV=EtQtnAQ#`f1;Hv*VX@hntsO&Bj6ZTlxF|zghDLdee8=lt~ zg`(9_xs)C7abdTxu+(Z#&(`dUIJEo!+B@&yDvR!qKimrlNGB>%LJLyvLs1aKmW zduKDlIP>LXIPT|cd3Mj9J?rFs79tb5az~%tpP$Y{(&-mC?jVOz+%>Cai}AX|~O^NG9m7vd?;EbK60tn_5v7Hk?dC+np`JZ5K&$j4w8(_kV2hS6j_K`-;N5 z64=i1STMm{fzZc@vf3$z4;c>$I8Q|6@kcq|vyi2CjW33@cWo4H*TuOS-Ih%7)q}Rf z`tC(%M1dSLrGMI<`LE z;`8%fz~hPRT|bh;$h&V7Z82k^CfSVb-P?+`JBx*!$AXERD?oNo47VL*>;>TF40Y$k z)!%a4jUahjnb?@C6)K2xA8NMUWQBKU2C#Q9k)tuZI~~T}LB@FnN_`h4$hBqQ!n@DnEkZJZ(>kZaNEvt4srr05o!-L3GhGmh&# z7EA*7?`qs&?|vj1S*gf_!s+p$B1{%?Wy&FY^-X$6ptL60jP2boincrRfZOhPTH9g1 zz<0$;3^ZN~GQKN%_RPxJGLs5&+d-~HYvcDT+hUXMzVYu0?~2EDz-%Yq_zKA<=nm-MQFPlalP1}W?cH5P+bvqcZ3h$d3XnzHZFEiq?vYIU zc7DtiSOwg9kI6!oZFgm>X4~Zm$2URnpPmKSyT{1{BZ^$#&3K2&LdGfpPczeBCDtj! z-a$S=eRfVKTP)LknmJ>8S4h!z7yaCJg~L*W+d+;W(mU-DhL%y|@^afju0?HkgZ2VS z>(*Mx*xofzv|Z1&^w_M&f{9#xVxCP+eT*BgA?aA8;DZ*WZ{B#D8xQhPYP`csZP6l5 zGw?FFd+ijhcM%soJQhr3uf;dx(Y)MvkZH#_3K=PAykkyAO8bDuThmCh@r-xD=kfGZ zaqFib`^=wA7AAo!yq^1+Y=4sJBbRT1f^h2;?iP^c{f`BAG|3w8LY3?YMcl18&2a}4 zjK}zcx$L^laW|4=tOHPE4mW838qaYDS!TLd)ZHU9-noyvDGIVH@q&xTiX#)bR{^p+ ziZWTq_-4dhfm;H<7iO}M7t!jt%OqR$)*Vrdckao4q99ua&k zIEzAM9#ogPk(J3p-bT4bv0wYd>ti*^8t+1t>=s4b^$O>>OGt}5oTp$npwVrPJIFXG zb86olin`&PZYKuBU31-X_{KZ;ahIYZdy>h*1dj`2kBlp%%ww{UaZ3QzWw5_^?lzAo zdr0n2bH%}Iwz#jGYh=80Pxgd@>?J2N04DN_5ob|zoYNl2xTA{G9<1*&JGH(}k}P+Q z^IMv8h4Ick*{cNNQ9orybDxC?R_@s8DG@)q=EyVLXCY&Y45y5^QTCN{bNd>}@ziGz zrKb9XwLlZCaZjgQ|9u7Al}@S_OaNAH9*@1qa3SMbJ`OE#Ex+meJRmX;!+>$;m z>QfQCORF@;6ysfRI_ZhKA_}t0QkX1EFq+7sP}6Yd$7>ZO8OiQQ=%ICeRG(2y7P2H; zu#YBL{maW?UV-m==a#HadlYE{^xOAZsE|>Oc z_SuZ>U3*2_MPQ%PV|5@C%w+h3PujYRxa}b0X%}o(;CTB#3EV3>l01}>@IEbPi-|im z+b&~!H$c&L-7RiAn4sM-UtsHa_-bxD$Y?uk9U~>Wu#emB^?1!R4zg^!xhFN-&Uoj3MA@St+oL#>-Ag9u6>_fF@;Q$vkkKo!e^=`G zTmE)Om@MS^)GIDiI?rI;D{sa-_he5g$d-1>eZd4&G2S61dcr8ml=fPXQJe=S0=Tx7 zn)(@|JxzLjnk*VMv&CWE;~wLl&T&r$Z@oVhv3J2Kga;EjUtk|h4Ie)sPa%jwue(55+-to5v!;>>zOQMoC=^M1m=r_-I#1@ zD(PXg#hCqVs)3_hz0$a+Gpgh+9y~Ch?!#%Z$AXDmBIEO7=2V88hh!Wgqh2pQ90!zS zxRB$BZTY^o=%IV3(RfD~g8ZcCFfY7#kY7xbn(Sn<#mEE;0m8?dJwqdzEM(j*lU4h3 zUSzV6Gt)B9yU-TTHPalBjdy7%{@;K8_sLeIFnQF^^shPYDv=3B6nr>hy7=%Q#~ozc z7Ql2t2UCf?Z*kl~mT@;EpJv<{?}D>QPuxWlh)4b8`H0EZBom|tNV0!TVzQ8{Pa4JV zmg7*R?ocLMi{$w7u`%~{+v32-nq-Z4!P%sTY!d?UsGoi9m@G_WpT)tzfMHCwDajy< z$_A7tm~f>ZlZ6~Z2Lrb&(K~Ot)kutY!P%sTY$pQosGlZ4=&@iDc;qwEsniFVUIdSx zzs=$d&==#FY-f^XHQ}4O)#i+M!P%sT>_7tXsGsSXnJi3@9w3it@1rYwtwAIs4G;4y ze{c_HvXEb(O3X*D`NV^7HAiFPU2r!4`(zU+Odj=<`x?hxBALjv6l^jkBg4#VL7r7` zEFU$}0wJn<4~{#?+31V$(-F4Fs$1LLco&>adg3mHKs@TFaY-h-hfD(Z0;()vvXHTI z$H4%q;5R!Qq8RUjvq=xx69nQ>KYMdBS(wN^i(Q^!uQS<`BqK=>2LssU znViIAA#bD2*^9Mokzcnmsqrp2oAi*qN+2HfllvVedyPzFdH`1L?_$r^Ye7bz#VaNB z-=t3KnCu@U%lEdj_tWgN#=GEb{`bk=r!aZc&$JdCcMr$}-xc_SRX~L+9Cr^%M(t1R z8)L3$x{~7#@>N=WX8+kI!Ut%^o$)R>oAkt8t`fn27wS~!1~*LPok7eMji)hL$RLZk z0{eI0Ml#vlB(I@sW$i3mblamz)_513PI|}|QIV~}eYPl>V6MO)%oREDSQB0EAsO$0 z;ENIe{#xgil5mon64~)NZE;Swj*IcmJy}~pwox4>TY*dhcX{qO^(!G$x^^_)jG`-f z{)x`Yys)(lJ%?NZXm}UOb~ZCW5it1rU{QI(Ilg8D&9cGVZrsDsrjtT z7UXQSZ+yABElQ=PY6hNhPbW2?kpgZmT$b@zjmZR41>oW!@3qAY7c$`9Zc&QMLKG{{ z3jxUT{!t#?jUMBj`$46Hg6veRh-I>n@zLmX+e1-dEto9iMYJ98 zSzTMq+ppPcjd$+ImasBe0~BN%(Rd29uagaSdSnV{_a%%LGAe!mExyYJTZ|TRW*U(P zzF>Y!ic{>ptBAc^M>zCgA`g+V7})CMl|#lRBW?_!z14|-nko9Kz5pfY|dUx7ABG`=8Jb7veQY%kq>T!A_Mec9^YL^686S@5vp7WlsVVo_Pf5N^+jui*8*gu!TJv0U)bzf=p=ruBU#=E#b2F$g~>vmPiwr&_2@CWFEz;;??RPqk|OR> zp5(YoCKD_`5qmfoxaSm}g^YV3c-|68f}J;T+(EuiuQKHSl#&GRYQ~-M&VAhNRgjHC zRcMd3k4!L6VA#N15jKkF3dpoYz(4=@y5QiU3 zFlFEa646)u6h~iPl5uyi-lhV4p1k=ik1UW+&{D4vJ-}6KgJ$#@?}D>QPo_XI1=-0} znQZYivY-pHaaouwWR#0cD-;!#)!FcYT%7I>HkoIO?z*=IjCbzIR#K3Cr7n}L96%PI zXNB)DS;*+KvM}C|o!n<3zd$oaysrh@G5eoVGj8d;3S_&oc!G?RsV zGGcu6V1IeO>CU5JNI$dSwVL9872!8d^uu7!I$IdrA!ub(YD>Adw)=Y zN0e`3m~0o4R}tCf6KzpFyCzxVoqMtaRb=lm*+FC?SMFE^^mFpIAmgpZ53b!$LXK~2 zeI`4YPFF&!cvls9k$s6gWPeZ)>*88F+ zTH~J1fnEl0z5R;VYw!oh-T^YfN4ot#@{2T6{jG=awTZ85pG zX6PC3+>a_}6=WZu&tzc&beJ5aA54QU=p|z80%F2fQ(v^xXq1!f0baee+7_@drPy=8t>eZ#f=NA zRPf)0Ix+1+NG7tua16Xy3LHVQzSn~MbmkAEXBEthtbG5&HjLIoa#`xM-G|%awY3^( zje9z+hs^hUD!8br>0uM>=7230@C%Uo>$URJZz6B&jo$hARlkz+nJr{F)m9&;h$(B&WU45JYc&w6Sf=x!b#LLr(;|?;Wi*H`Glc~61Qtp}@caZDT3$Ukl`9!Yw zHRH~B7ktQ1PvXb33bH#<_uFGVMu8cS-i0dJu8O$J zR+!@sCi2J!ad$hJldCYmIlNf=8Cg#bKF73 zD&WF?4_2QEExzKoTSoFB+FxAO*%nh)XvUrK&VAf{sUqv#3WbR*CB*3gJD4nF>;)j9 z9El%|3o_Y_0c3~v(j;rVb5Hhr1=&eXZ3vjiGXaoIOX=B0GWG(nZ;XHcfOEYE@=;3c zy)()dnTKhTHQu==`;&rfcBlFROal9C%60Cuhe+OZGco%4j)l0;`^A0SXCccK6m z$r|rMmF%yIxZBl>R{=0V+{rJ-DK|Op&Xc?+C63F^VJ{$JFvlI_D5@jWlpdprKdc#d z#yj_McT++3Wv4>aEi%CuqdY&}J%!0a#_l3g1Mpq3-XRNl29d3L&L^5Zt4Y>)=bo%L zBKYq@&776{Y-Tb6RrFcx1$01`kJo~X2c+nwzhp}AP0IQW_gTo2Y=my2mhmn)o%EdI z6;O~Jiy~hhD=dvH=;AHzU!B@tkdglBJm=RkF<*Bk3ppG0*~+(k;zDiBK5M)SRkEcN zaTn7j^$Ci*h_txF=`p@5Qd8^kkOCc>P}n#|J(4{>4!qJNm!<8+iLcsXkM1X>IWwLciPN39)@!$AFv{#d?@yq3&uhk=@y>nR4ONiM`~#DP3AO`} z`iWJ*HRk|%7|FN;f=`Qq`MpsG!k8>%Np@*zO|r&2_hiQ^$Yya~3WtfDD^L@n{wnUX z<4C3ro~Yw@9`fy;asM(|$TF$-4c!~u#yj_9XDi5#!;|11YYv&@II{G=>$~L(nJi@7 z;Kqx7NctG*tO7nH`39{5MjiHviQjAXS>v61vda}@e<;UfVIuG5V+*g5Q`;3X)^~YZ zKb?$p?d!w1&#oZ3AdxNbmru;et4Y>)7pi2xQp85!wsrRlpYOCTYf<@y>nR{h%P5Whaw`N#H@=&5Ar%Y$ut3N2788MwF>e zf*@o`cK=RIvc^02WDhIIj=RWYVFI$~x0oxklwz_!lZ=%+Du3d6rjtjw&q6Lpd$TKE zvqjY@nq-Z4?#W(IkgfX>lZA;~xr6LzY}k6OizE+E`qYmjTI}+aI>TfkM^Or1@=#l3 z&!tJ$co(W<|5n7^4_|ZK{X-@)jSO)&xdX=?WbE>weh@7X62CjhaR*seiEcYhGwzIc z?&B^?>EOQ$wVspunUzd1PsoG3dc~M5WK{mdvtV>GkaRZ-lZ8B#=86#wY_T|AldSPB zIGyyQ20Wo4yWM%j=Seb=eHJxb4zy*mkjp$fkrTD>;M<##Oct^v+i|caS>v61vd^f< zuI14fCb*Q3ev71!{Z2JE$kiu}`tNA`rn56!hU7Tf3)n)5T7P_}N!EB5s${Dv;;!EN z9Ct9miGZ9dzINt{>Lf3`zB0N>l`4Dzr0)P8Q6TT6i$`O^ZSiGw&A2n(xsSVg3bI*I z4%%bEM6Lp`zH5m4=3bVGD(@R_4j-3Tdo7cFiL4Wd?9;_v61vaJ+k$9G|} zFhQS1#s^jbRH+4IUn3chG~(STtlTF#55PknOZ#`rvrx_cXEezg@7$B^sUTY~FO%&> zCRpErF3xA8Dl=Kg)Rm*-hh$^2FTBNlwl~QU)Mu+!wMEt|nq-Z4p-T2mMci$V=C~V9 zCfI|*h=RGI#%zu|$ao@r;q@}f*o*4+J;z-v$?~m;MSC^l&UoiO?#3&~?wH18Cy)uK z%Jp5Ju}l^+E`T6AuzTcb|D;Szc47e8#ZxuO8t>eb{YXXDDdPqcxxT~I?LK}c3mK~b zoZ{X8?9aq%&Na}vB;TjGV#!msNRHJcYrJz$cBO*sV@_d6n1C$)U^L!=Eds9vnYyyS zao4;UjT4()=03ZM1t9L;xW{p~nPklV zlts>$1^>>)aR<36m4v#r!YAGspc!|@JNI$7LqT>&6($Q4`NfFuisNmVEM#PS;JX5~ zznA1?vO7txOm_rw?D2{HZ)=h@-nl1xL_zkYqD&Shk}Ue{HGD{VEyyU~f?^Pq(VOtr z1SWelfNZ5Lnq-Z4?#W(Kke$$<$-+dQ^`Ot%VNCWi$vB_IDgb1^b^0u1dHdtNGn!kHDW^m{bp&UoiO z?jCz4`0qmf7WF(lRyH!hHwE?sKz2@HCJPyv!WLi*JiSiaR;>i*}MEA=yfOK1M*J#V(e7Q7N;(0 zk~QABCtF5AcGMasTb4{@-Y5EOr5;Qcvb-a}Sz492&z1`y+o7!{S>v61vM(seX8M@P z!UXdy;o~yByPfqNWW3*p+r6}pmpI@HCR>B#F#6j5b*)d-ETT!)co(W<>nq}}b}NoM zm>}-362QK3ERKM@*2^Tf?H8gFiU#Aj?N0p3;36+<50c?piCzejCeVVFIe4 zi@kvDpE21sBzKSOl(x&0C$V#XCJVVN-P`Uq*(d(ieb~i#=bmhD1=&p5nJi3jf+zdz zKX@+OYxN-+tAJ9+Yw-E(nzl?9@@>laX#9^)EU2Y9R~YZylZ{o79aV2IjZQI#m{c%mQ#=B4@J5dpL?>o1SCZ)xl98oq5 z<+y{4FUG6Q3ZfpAKf>7yfGp!Key(QR8SmW3-CPA(%emJB6HvuhBeDZ)tY@+xlZ>+- zJQam~{jP??`LEpcX=>Z^h6l}uLYTQWqGbZ zG1uWwGFiwLkzG5|7Nuuvk~QABC;PRE>;*#Tv0#Eei~T#0ZGb{dUY1evbHjq`N$9hk z>N44H$a*J{eX%R;x$9PfFy4hK*eb{Y^o3t5fOgcQV1+4*eFDzQ$!@vXJwSsm15Bs07hy9g~GD$u6I% zN!EDhp6ne3*;-Deue)R-R{_|W?beLRrpa8R7bgPqA2M0UTg%19eDk|4zPPVR)_51H zWV4qE{(lDb`zLs=$U!C;P2?ScrLS?^LB?EhyG1F^`@CP4;|_9c+1QvOPgW3pb)TR$ z-UX+Vp1goU3bJEx8Ng$~B=GdO?Xyf4GSz|dfB*O8B-9Id!Fm3pFv+v&yP|L(TMQ4= z98rvS?#Y%{kQEJ?EKCAVk9#>8y^txBm-g>&lH^5H3F65!J~8(bO|r(jP$m0{ zBJRd~!Epx@nHPXPsLEe)+({W#gJ^pyYU-c73TO}zcb9(Aj636<`?zbXAnP5&WMP64 zMP?fH?#*P=WX=nqrC;K2Lz!$lvd&8*N{O33aq_w*S>v61vV9d~zyFKL!X$8)r#jyA z^IDJx`*TEpaP58)PLCV6VzT{69z?4EPeWToyr)Ulc;}w%+X}Mc1e1k{B#Y5FlT*oc z1j%@>2V`l15NTy)vXCbd*`Mp%BAad{SL0o%lAWxGyE@LTBbWq!F;?rqBgzz#vHHZV zBia*4JTa2v4sry=U7l99cvp87V7zl5ck>ivzpKL|3QVvHkh?q;Z!y_VNJhp-tK7Lz zXVyPtJClVh$&M`xWLDu8k?u7}y7O}p=DPB0bve#NoGBSGWpM8(M<%EZjqE_I0($OavXF5;i(GBw1-$(?lf556_RFK1 zeb#vAp6uggga0nnxlX-Lm|!Hv8V{=g&uu1~lVq#{ru2V|`)s|RnJnZPG#Xc>s^6c7+vjSJ0{VS%gfm3TR!UD#zVk94dP)$oQ^Edow!mVbRolR%VuM zin{^VeWHA7s%GFB_p$_!JS#n+=TpEv>JYZbr1z)W1itJ^wkaNQEKJZ}vBtwHU@G48 z^IDLxb&S*itOANT$2~~_WC!U!@@l+uPj-)j?8m2=EKEQZ`vD;P`2r>j84vdzzZ1>{ zT#kRreReO&ag-YHQD$1X&(|D{jd!6+_M{^2p1#3x2NT2{4IeyLZ2y$w?i9(mO6Je; zR5Idk%S#-0kXzCh1tr+!^oO$K5pr*<+)aEKKD34oM$FvFYfwAmb6RRofrO zF=Il5Pnhf=bZ;JeT_H=6bexd9o&1$Mesh{}wllw5)CM3_Nvz}KYY_V&j zCRyX1d$JuBWD5;vvM@nXFXmZfeEf_X-Chgwo|L%%rfXmQfys6vIf1gY4s^4{<0+bC zjd!6+_H{+v{gRdA4kj2;u)Bye#XuO7B&rTZ9@@h()!u2sZc;T;@z z>&Qf2DZ_};%egEF8LI$11&Z>}t(=QTkgw8r#nz{7QDCa(h+@2RA9qO#vY-6MWRuB6 zUIPW$>9t7qT9C1Cx8jos&iH6BlSdTDlI+!nnq-Z4?#b>|kUjnkliim_7VA6Y@^}U? zS;#o!!Tufk?2g+^7IInInVtBmEhcQ&Bx}5LPxiEm?Bh%pCg`(R_ZV{qzAoaN?9XROc1{{^^AmI( zPjcKrZcUFz=NRh~qn2yNo$=0n+|^Q$J&8|$j|CHX;sdfPO7MsR8Mk|6VW>qbnQU#6 zGd~|2^U469=%$+;V7zlrwyA<_2UKnISTF%u*=L(L-;K>k#wq~W0W?=6-M-3XA!noe zf^Wt9#D8*W_F3bdd$OGsWET`;vR%j|a5OI7mHRAYWt<8G9K?EKM87ADwVlyk)rT>9}^kV_rE zv4ZT0lS~#S=&vA)J0R7LFxe%J%r#t4B>G_+CJR{>c8|NKN!EB5s$|zI;;#Kojyssh z)hEsc2F7#TZ6FzE$ap-Owx|+6xWaJWYIti$u>FJpXW%X zuFQG8s0uZ~`6h#0mUelf*Z9OG-FgtlJNIP&RFFO4)LnuJ@;=ZpLAGWM?z4Z9jO@?N z;aT|n{iB=QXCZGTvJrEAqPXt!%*MM=C7a0?{QnGUCoj(x9x}lyK;9~g+0StY8LI$P z5X4W+ILhbQLQh5PdQ6k7@yCR-(dY&G4yUgKS;lC7hNyJEQV>9JrU=L+NnWWURC2N_?CGV8e7MUFc`@_gDi ze#>Kvd#f}@6yu%yxNEK;`{^7e3losVh=Nss9m`}PBZmxCp^(>mZvc~RL9)#2&30Uq ztnto0*{%w*oz62^m|(7uyF4lXGFiwVi@X5Ta`|;AlkGiE}uTQJjK z9CwFEZcigh*l#{D>8fVj8SmW3-LDF=3!U0QFbQ0J<~zY7%6XD;x19C?oC-6|m@H&T zcGntBvc^02WN#|ShC8)`U=nzWH{(?%dy8be8Hv3B+9rtFlEP#mSEWAN=?|Y+kfKS} zc;}w1*AD)>P+Q#LRRByd8e1`w5A~9t$RzD-d@$5r}s3jl)Qm*@37K{kh<{gM5|tpkh1NqRB|j zxHI0lkGoO|vISN!*$6Vho-zL5yJC!!X#|;4LHR_0Dt;thaxy+3cc*=nxs7a*QdyI% z@yDGoc-nl0m zqaZu00h5J^?6cVWZ04-*AQx@homcJ{jgJ)P(YS5^+4w@5eb#svs$^R#;;ujsjyssh zJ$H;KrEjD@LHov#>2o7(L>UvgKQ)yD?^V*b(T7Eg<391HZqa$;o=)MQ?h3d+g)v;1 z$Ydid@h08o=>jsg1LnSdkju;DbDjn7L2?YSt$o!edezq)RE&4-2bG};vMpX@vM`a| z7RQW-N;27DB;&Y87K517ihC{OTeJ|E`l2nq(tQxrc;}w%SOwYUlbI|`0+)Ej0o-ei zBN-Qbae$1}&vHpj7IG|+z4?kQhMd#vv&K93WM?bL9$dy`VG`J9+dIcSb4W&?ZIwG0 zU$hicm@MQIc5KYxQMRb1dv)7*=br2`1=;+EnJi2sSuCQuY~((>oaC4({i6r_%k%Th z!=GoekR@3wzhz?{;kJW3h_<0l z4Y0+{a++ receivedData = new (); - private bool isDisposed = false; - - public event EventHandler ReceivedData; - public TelegrafUDPStream(IPEndPoint Receiveendpoint, IPEndPoint transmitEndpoint) - { - client = new UdpClient(Receiveendpoint); - client.Connect(transmitEndpoint); - var token = cts.Token; - task = Task.Factory.StartNew(async () => - { - while (!token.IsCancellationRequested) - { - try - { - var receiveTask = client.ReceiveAsync(); - var timer = Task.Delay(500); - - var finishedTask = await Task.WhenAny(receiveTask, timer); - if (finishedTask == receiveTask) - { - var result = receiveTask.Result; - receivedData.Enqueue(result.Buffer); - ReceivedData?.Invoke(this, new EventArgs()); - } - } - catch (Exception) - { - - } - } - }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - } - - ~TelegrafUDPStream() - { - Dispose(); - } - - public byte[] ReadMessage() - { - if (receivedData.TryDequeue(out byte[] ret)) - { - return ret; - } - else - { - return Array.Empty(); - } - } - - public async Task ReadMessageAsync() - { - TaskCompletionSource tcs = new(); - void f(object sender, EventArgs args) - { - tcs.SetResult(ReadMessage()); - } - ReceivedData += f; - byte[] ret = await tcs.Task; - ReceivedData -= f; - return ret; - } - - public void SendData(DataMessage msg) - { - string name = msg.Name; - byte[] data = JsonConverter.ConvertDataToJSONByte(msg, ref name); - client.Send(data, data.Length); - } - - public async Task SendDataAsync(DataMessage msg) - { - await Task.Run(() => - { - SendData(msg); - }); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - if(!isDisposed) - { - cts?.Cancel(); - task?.Wait(2000); - try - { - client?.Close(); - } - catch (Exception) - { - - } - isDisposed = true; - } - task?.Dispose(); - cts?.Dispose(); - client?.Dispose(); - } - } -} diff --git a/xcmparser/WebSocketStream.cs b/xcmparser/WebSocketStream.cs deleted file mode 100644 index 3fdc366..0000000 --- a/xcmparser/WebSocketStream.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using WebSocketSharp; -using WebSocketSharp.Server; - -namespace xcmparser -{ - internal class WSDataStream : WebSocketBehavior - { - private WebSocketStream ws = null; - protected override void OnOpen() - { - Console.WriteLine($"Websocket connection established: {ID}"); - } - - protected override void OnClose(CloseEventArgs e) - { - ws?.DisconnectWebSocket(this); - Console.WriteLine($"Websocket disconnected: {ID}"); - } - - protected override void OnError(ErrorEventArgs e) - { - Console.WriteLine($"Websocket error on: {ID}"); - Console.WriteLine($"Error Message: {e.Message}"); - } - - protected override void OnMessage(MessageEventArgs e) - { - } - public void ConnectTo(WebSocketStream ws) - { - this.ws = ws; - ws.ConnectWebSocket(this); - } - - public new void Send(string data) - { - base.Send(data); - } - } - internal class WebSocketStream : IDisposable - { - private readonly WebSocketServer ws; - private readonly List wsStreams = new (); - private readonly Mutex streamListMutex = new (); - public WebSocketStream(IPEndPoint endpoint) - { - ws = new (endpoint.Address, endpoint.Port); - ws.AddWebSocketService("/data", (s) => - { - s.ConnectTo(this); - }); - ws.Start(); - } - - public void ConnectWebSocket(WSDataStream ws) - { - streamListMutex.WaitOne(); - try - { - if (ws != null && !wsStreams.Contains(ws)) - { - wsStreams.Add(ws); - } - } - finally - { - streamListMutex.ReleaseMutex(); - } - } - - public void DisconnectWebSocket(WSDataStream ws) - { - streamListMutex.WaitOne(); - try - { - wsStreams.Remove(ws); - } - finally { streamListMutex.ReleaseMutex(); } - } - - public void Send(string data) - { - var removeList = new List(); - streamListMutex.WaitOne(); - try - { - foreach (var stream in wsStreams) - { - if (stream.State == WebSocketState.Open) - { - try - { - stream.Send(data); - } - catch (Exception ex) - { - Console.WriteLine($"Websocket sending failed: {ex.Message}"); - } - } - else if (stream.State == WebSocketState.Closed) - { - removeList.Add(stream); - } - } - foreach (var stream in removeList) - { - DisconnectWebSocket(stream); - } - } - finally - { - streamListMutex.ReleaseMutex(); - } - } - - public void Stop() - { - ws.Stop(); - } - public void Dispose() - { - Stop(); - } - } -} diff --git a/xcmparser/pruefstand.xml b/xcmparser/pruefstand.xml deleted file mode 100644 index f36f73a..0000000 --- a/xcmparser/pruefstand.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - fileinput - LOGFILE.BIN - - - smp - - - - - - - console - - - udp - 10.0.1.131:8094 - 0.0.0.0:7000 - - - - - - - - sharkSerial - telegrafStream - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 005cb9c9caf068251c339132becab5ead4325716 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Tue, 14 Nov 2023 18:27:05 +0100 Subject: [PATCH 19/27] Update libconnection --- libconnection | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libconnection b/libconnection index 026b54b..3dd7897 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit 026b54b700fdc9ba189ecbc9b60122a59f85985b +Subproject commit 3dd78976e343e5f3e438f6b3c6400d5b1d8ea289 From 1869c2d1c0b4ebbe8cd5a1a7f6460ea3a4a4e06e Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Sun, 19 Nov 2023 18:45:38 +0100 Subject: [PATCH 20/27] Changed libconnection to async model --- libconnection | 2 +- libxcm/Connection.cs | 7 ++--- libxcm/XCMDokument.cs | 32 ++++++++++++++----- xcmparser/Program.cs | 72 ++----------------------------------------- xcodegen.sln | 10 ------ 5 files changed, 31 insertions(+), 92 deletions(-) diff --git a/libconnection b/libconnection index 3dd7897..613906c 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit 3dd78976e343e5f3e438f6b3c6400d5b1d8ea289 +Subproject commit 613906cc573dee7877aba3ef03b2ed041a0b0197 diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs index 49609d7..98c2797 100644 --- a/libxcm/Connection.cs +++ b/libxcm/Connection.cs @@ -9,8 +9,8 @@ namespace libxcm { public class Connection : StreamPipe { - private object _lock = new object(); - public Connection(XmlNode node, CancellationToken token, bool reverse = false) : base(token) + private readonly object _lock = new (); + public Connection(XmlNode node, bool reverse = false) : base() { var iterator = node.GetChildsOrdered(); if(reverse) @@ -63,8 +63,7 @@ public Connection(XmlNode node, CancellationToken token, bool reverse = false) : public void TransmitData(Message msg) { - object additionalData; - var data = MessageConverter.ConvertToByteArray(msg, out additionalData); + var data = MessageConverter.ConvertToByteArray(msg, out object additionalData); var convertedMsg = new libconnection.Message(data) { CustomObject = additionalData diff --git a/libxcm/XCMDokument.cs b/libxcm/XCMDokument.cs index 5e896e0..52f2fc9 100644 --- a/libxcm/XCMDokument.cs +++ b/libxcm/XCMDokument.cs @@ -2,23 +2,24 @@ using System.Collections.Generic; using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Xml; namespace libxcm { public class XCMDokument { - private List symbols = new List(); - private Dictionary tokenizers = new Dictionary(); + private readonly List symbols = new (); + private readonly Dictionary tokenizers = new (); - private Dictionary incommingConnections = new Dictionary(); - private Dictionary outgoingConnections = new Dictionary(); - public XCMDokument(XmlDocument doc, ITokenizerFactory factory, CancellationTokenSource cts) : this(doc.DocumentElement, factory, cts) + private readonly Dictionary incommingConnections = new(); + private readonly Dictionary outgoingConnections = new(); + public XCMDokument(XmlDocument doc, ITokenizerFactory factory) : this(doc.DocumentElement, factory) { } - public XCMDokument(XmlElement root, ITokenizerFactory factory, CancellationTokenSource cts) + public XCMDokument(XmlElement root, ITokenizerFactory factory) { foreach (XmlElement incon in root.SelectNodes("/spacesystem/connections/incomming/connection")) //Read all incomming connections and generate connection elements from it { @@ -31,7 +32,7 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory, CancellationToken { throw new ArgumentException("Each connection element must have a unique name"); } - var connection = new Connection(incon, cts.Token); + var connection = new Connection(incon); incommingConnections.Add(connName, connection); } @@ -46,7 +47,7 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory, CancellationToken { throw new ArgumentException("Each connection element must have a unique name"); } - var connection = new Connection(outcon, cts.Token, true); + var connection = new Connection(outcon, true); outgoingConnections.Add(connName, connection); } @@ -66,6 +67,21 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory, CancellationToken } } + public async Task StartConnectionsAsync(CancellationToken token) + { + List tasks = new(); + foreach(var connection in incommingConnections.Values) + { + tasks.Add(connection.StartStreamAsync(token)); + } + foreach (var connection in outgoingConnections.Values) + { + tasks.Add(connection.StartStreamAsync(token)); + } + + await Task.WhenAll(tasks); + } + public IEnumerable GetTokenizers() { return tokenizers.Values; diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 74968fc..4489d75 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -39,12 +39,11 @@ class Options } class Program { - static int Main(string[] args) + static async Task Main(string[] args) { using CancellationTokenSource cts = new(); Options opts = null; bool optionsValid = false; - Task receiveTask = null; string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string workingDir = Directory.GetCurrentDirectory(); @@ -80,66 +79,9 @@ static int Main(string[] args) } var tokenizerFactory = new XCMParserTokenizerFactory(); - var xcmdoc = new XCMDokument(doc, tokenizerFactory, cts); + var xcmdoc = new XCMDokument(doc, tokenizerFactory); - while(!cts.Token.IsCancellationRequested) - { - Thread.Sleep(1000); - } - - Thread.Sleep(1000); - - /* - Console.CancelKeyPress += (_, args) => - { - args.Cancel = true; - cts.Cancel(); - Thread.Sleep(1300); - }; - Console.WriteLine("Startup successfull"); - while (!token.IsCancellationRequested) - { - if (opts.EnableInterace && Console.KeyAvailable) - { - var key = Console.ReadKey(true).Key; - switch (key) - { - case ConsoleKey.T: - bool enableInterface = false; - lock (opts) - { - enableInterface = opts.EnableInterace; - } - if (enableInterface) - { - lock (opts) - { - opts.Verbose = false; - } - CommandEditStateMachine(tokenizer, pipe); - Console.Clear(); - lock (opts) - { - opts.Verbose = verbosity; - } - } - break; - } - } - if(receiveTask.IsCompleted) - { - if(receiveTask.Exception != null) - { - foreach(var e in receiveTask.Exception.InnerExceptions) - { - Console.WriteLine(e); - } - } - ret = -3; - break; - } - Thread.Sleep(10); - }*/ + await xcmdoc.StartConnectionsAsync(cts.Token); } else { @@ -154,14 +96,6 @@ static int Main(string[] args) } cts?.Cancel(); Console.WriteLine("Shutting down"); - try - { - receiveTask?.Wait(1000); - } - catch(Exception) - { - - } return ret; } diff --git a/xcodegen.sln b/xcodegen.sln index f007ddc..0b3c2bd 100644 --- a/xcodegen.sln +++ b/xcodegen.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xcodegen", "xcodegen\xcodegen.csproj", "{B2BE3377-883E-484E-B22C-4D7C18A03E78}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "libxcm", "libxcm\libxcm.csproj", "{7E825ACA-B34F-4F11-B160-AC9745DE6B6F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "xcmparser", "xcmparser\xcmparser.csproj", "{CCF105B3-FC6E-4BB9-BAE1-85B3320E9640}" @@ -23,14 +21,6 @@ Global Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B2BE3377-883E-484E-B22C-4D7C18A03E78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2BE3377-883E-484E-B22C-4D7C18A03E78}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2BE3377-883E-484E-B22C-4D7C18A03E78}.Debug|x64.ActiveCfg = Debug|Any CPU - {B2BE3377-883E-484E-B22C-4D7C18A03E78}.Debug|x64.Build.0 = Debug|Any CPU - {B2BE3377-883E-484E-B22C-4D7C18A03E78}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2BE3377-883E-484E-B22C-4D7C18A03E78}.Release|Any CPU.Build.0 = Release|Any CPU - {B2BE3377-883E-484E-B22C-4D7C18A03E78}.Release|x64.ActiveCfg = Release|Any CPU - {B2BE3377-883E-484E-B22C-4D7C18A03E78}.Release|x64.Build.0 = Release|Any CPU {7E825ACA-B34F-4F11-B160-AC9745DE6B6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7E825ACA-B34F-4F11-B160-AC9745DE6B6F}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E825ACA-B34F-4F11-B160-AC9745DE6B6F}.Debug|x64.ActiveCfg = Debug|Any CPU From dfa54a11cf819a4803419f859b8d0197779f1c63 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Sun, 19 Nov 2023 23:22:30 +0100 Subject: [PATCH 21/27] Update libconnection --- libconnection | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libconnection b/libconnection index 613906c..687d314 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit 613906cc573dee7877aba3ef03b2ed041a0b0197 +Subproject commit 687d3143625bf66bbf584105e8c760a074c7f418 From 1f88310f9b79cd5c904cc6146f0ca6c1c3a8d95b Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Sun, 19 Nov 2023 23:23:05 +0100 Subject: [PATCH 22/27] Add some improvements for mqtt --- libxcm/Connection.cs | 34 ++++++++++++++------ libxcm/IMessageConverter.cs | 4 +-- libxcm/JsonConverter.cs | 14 ++++++-- libxcm/MqttJsonConverter.cs | 44 ++++++++++++++++++++++++++ libxcmparse/DataObjects/DataMessage.cs | 2 +- 5 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 libxcm/MqttJsonConverter.cs diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs index 98c2797..e8ebbad 100644 --- a/libxcm/Connection.cs +++ b/libxcm/Connection.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading; using System.Xml; @@ -21,8 +22,9 @@ public Connection(XmlNode node, bool reverse = false) : base() { if(stage.NodeType != XmlNodeType.Comment) { - if(stage.Name.ToLower() == "stage") + switch(stage.Name.ToLower()) { + case "stage": string className = ""; Dictionary parameters = new (); foreach (XmlNode parameter in stage) @@ -54,28 +56,40 @@ public Connection(XmlNode node, bool reverse = false) : base() throw new ArgumentException($"There is no connection class with the name {className}"); Add(datastream); + break; + + case "converter": + MessageConverter = GetConverterFromName(stage.InnerText); + break; } } } } + public static IMessageConverter GetConverterFromName(string name) + { + switch(name.ToLower()) + { + case "json": + return new xcmparser.JsonConverter(); + case "mqttjson": + return new xcmparser.MqttJsonConverter(); + } + throw new ArgumentException($"No message converter with name {name} is available"); + } + public IMessageConverter MessageConverter { get; set; } = new xcmparser.JsonConverter(); - public void TransmitData(Message msg) + public void TransmitParsedData(Message msg) { - var data = MessageConverter.ConvertToByteArray(msg, out object additionalData); - var convertedMsg = new libconnection.Message(data) - { - CustomObject = additionalData - }; - TransmitMessage(convertedMsg); + MessageConverter.SendConvertedMessage(this, msg); } - public void TransmitDataSynchronized(Message msg) + public void TransmitParsedDataSynchronized(Message msg) { lock(_lock) { - TransmitData(msg); + TransmitParsedData(msg); } } } diff --git a/libxcm/IMessageConverter.cs b/libxcm/IMessageConverter.cs index ba2d74e..0ee1a85 100644 --- a/libxcm/IMessageConverter.cs +++ b/libxcm/IMessageConverter.cs @@ -3,12 +3,12 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using libconnection; namespace libxcm { public interface IMessageConverter { - public byte[] ConvertToByteArray(Message msg); - public byte[] ConvertToByteArray(Message msg, out object additionalData); + public void SendConvertedMessage(StreamPipe pipe, Message msg); } } diff --git a/libxcm/JsonConverter.cs b/libxcm/JsonConverter.cs index 66d6d32..f4f47c5 100644 --- a/libxcm/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -83,14 +83,14 @@ public byte[] ConvertToByteArray(Message msg, out object additionalData) string name = string.Empty; additionalData = null; var output = ConvertDataToJSONByte(msg, ref name); - if(!string.IsNullOrWhiteSpace(name)) + if (!string.IsNullOrWhiteSpace(name)) { additionalData = name; } return output; } - public static void GetDataFromJson(IEnumerabledata, Command com) + public static void GetDataFromJson(IEnumerable data, Command com) { GetDataFromJson(data as byte[] ?? data.ToArray(), com); } @@ -106,5 +106,15 @@ public static void GetDataFromJson(string json, Command com) var jsoncommand = JsonSerializer.Deserialize(json); jsoncommand?.FillCommand(com); } + + public void SendConvertedMessage(libconnection.StreamPipe pipe, Message msg) + { + var data = ConvertToByteArray(msg, out object additionalData); + var convertedMsg = new libconnection.Message(data) + { + CustomObject = additionalData + }; + pipe.TransmitMessage(convertedMsg); + } } } diff --git a/libxcm/MqttJsonConverter.cs b/libxcm/MqttJsonConverter.cs new file mode 100644 index 0000000..ce49e61 --- /dev/null +++ b/libxcm/MqttJsonConverter.cs @@ -0,0 +1,44 @@ +using libxcm; +using System.Text; +using System.Text.Json; +using libxcm.JsonTypes; +using System.Collections.Generic; + +namespace xcmparser +{ + class MqttJsonConverter : IMessageConverter + { + private static void AddTag(ref string topic, string tag) + { + topic += "_" + tag; + } + + public void SendConvertedMessage(libconnection.StreamPipe pipe, Message msg) + { + foreach (Symbol symbol in msg) + { + foreach (Entry entry in symbol) + { + string mqttTopic = msg.Name; + if(!mqttTopic.EndsWith("/")) + { + mqttTopic += "/"; + } + if(!symbol.IsAnonymous) + { + AddTag(ref mqttTopic, symbol.Name); + } + AddTag(ref mqttTopic, entry.Name); + string json = JsonSerializer.Serialize(new + { + value = entry.GetValue() + }); + pipe.TransmitMessage(new libconnection.Message(json) + { + CustomObject = mqttTopic + }); + } + } + } + } +} \ No newline at end of file diff --git a/libxcmparse/DataObjects/DataMessage.cs b/libxcmparse/DataObjects/DataMessage.cs index e86fb37..303e092 100644 --- a/libxcmparse/DataObjects/DataMessage.cs +++ b/libxcmparse/DataObjects/DataMessage.cs @@ -71,7 +71,7 @@ public void ParseMessage(IEnumerable message) } } Received?.Invoke(this, new EventArgs()); - outbound.TransmitData(this); + outbound.TransmitParsedData(this); } public bool CheckValidity() From 76c3314484b02ea735935682064bf2f1dcda784f Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Sat, 2 Dec 2023 16:54:32 +0100 Subject: [PATCH 23/27] Enable csv output Addresses issue #1 Currently untested --- libxcm/CSVConverter.cs | 34 ++++++++++++++++++++++++++++++++++ libxcm/Connection.cs | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 libxcm/CSVConverter.cs diff --git a/libxcm/CSVConverter.cs b/libxcm/CSVConverter.cs new file mode 100644 index 0000000..8ed12d3 --- /dev/null +++ b/libxcm/CSVConverter.cs @@ -0,0 +1,34 @@ +using libxcm; +using System.Text; +using System.Text.Json; +using libxcm.JsonTypes; +using System.Collections.Generic; +using System.IO; + +namespace xcmparser +{ + class CsvConverter : IMessageConverter + { + public void SendConvertedMessage(libconnection.StreamPipe pipe, Message msg) + { + foreach (Symbol symbol in msg) + { + using MemoryStream memorystream = new(); + string symbolbasename = msg.Name; + if(!symbol.IsAnonymous) + { + symbolbasename += "_" + symbol.Name; + } + StreamWriter sr = new(memorystream); + sr.Write(symbolbasename + ";"); + foreach (Entry entry in symbol) + { + sr.Write(entry.GetValue().ToString() + ";"); + } + sr.WriteLine(); + string csvline = Encoding.ASCII.GetString(memorystream.ToArray()); + pipe.TransmitMessage(new libconnection.Message(csvline)); + } + } + } +} \ No newline at end of file diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs index e8ebbad..7a13914 100644 --- a/libxcm/Connection.cs +++ b/libxcm/Connection.cs @@ -74,6 +74,8 @@ public static IMessageConverter GetConverterFromName(string name) return new xcmparser.JsonConverter(); case "mqttjson": return new xcmparser.MqttJsonConverter(); + case "csv": + return new xcmparser.CsvConverter(); } throw new ArgumentException($"No message converter with name {name} is available"); } From 0ed6cfe5b88827075e2052fea7f63dd4a4537e31 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Wed, 5 Jun 2024 23:42:29 +0200 Subject: [PATCH 24/27] Convert to new libconnection structure --- libconnection | 2 +- libxcm/AggregateMessage.cs | 12 ++ libxcm/CSVConverter.cs | 4 +- libxcm/Command.cs | 4 +- libxcm/Connection.cs | 98 ---------- ...ssageConverter.cs => IMessageFormatter.cs} | 4 +- libxcm/ISystemFactory.cs | 15 ++ libxcm/ITokenFactory.cs | 16 ++ libxcm/ITokenizerFactory.cs | 13 -- libxcm/InboundConnection.cs | 77 ++++++++ libxcm/JsonConverter.cs | 4 +- libxcm/Message.cs | 11 +- libxcm/MessageConverterNameResolver.cs | 71 ++++++++ libxcm/MqttJsonConverter.cs | 67 ++++--- libxcm/OutboundConnection.cs | 63 +++++++ libxcm/OutputFormatterAttribute.cs | 23 +++ libxcm/System.cs | 134 ++++++++++++++ libxcm/SystemFactory.cs | 48 +++++ libxcm/XCMDokument.cs | 68 ++++--- libxcm/XCMFactory.cs | 22 --- libxcm/XCMTokenizer.cs | 168 ------------------ libxcmparse/DataObjects/DataCommand.cs | 6 +- libxcmparse/DataObjects/DataMessage.cs | 4 +- libxcmparse/DataTokenFactory.cs | 29 +++ libxcmparse/ISerializer.cs | 18 -- libxcmparse/RecieveDataHandler.cs | 37 ---- libxcmparse/XCMParserTokenizer.cs | 38 ---- libxcmparse/XCMParserTokenizerFactory.cs | 24 --- xcmparser/FMSXCM.xml | 94 ++++++++++ xcmparser/Program.cs | 14 +- 30 files changed, 697 insertions(+), 491 deletions(-) create mode 100644 libxcm/AggregateMessage.cs delete mode 100644 libxcm/Connection.cs rename libxcm/{IMessageConverter.cs => IMessageFormatter.cs} (61%) create mode 100644 libxcm/ISystemFactory.cs create mode 100644 libxcm/ITokenFactory.cs delete mode 100644 libxcm/ITokenizerFactory.cs create mode 100644 libxcm/InboundConnection.cs create mode 100644 libxcm/MessageConverterNameResolver.cs create mode 100644 libxcm/OutboundConnection.cs create mode 100644 libxcm/OutputFormatterAttribute.cs create mode 100644 libxcm/System.cs create mode 100644 libxcm/SystemFactory.cs delete mode 100644 libxcm/XCMFactory.cs delete mode 100644 libxcm/XCMTokenizer.cs create mode 100644 libxcmparse/DataTokenFactory.cs delete mode 100644 libxcmparse/ISerializer.cs delete mode 100644 libxcmparse/RecieveDataHandler.cs delete mode 100644 libxcmparse/XCMParserTokenizer.cs delete mode 100644 libxcmparse/XCMParserTokenizerFactory.cs create mode 100644 xcmparser/FMSXCM.xml diff --git a/libconnection b/libconnection index 687d314..a82f328 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit 687d3143625bf66bbf584105e8c760a074c7f418 +Subproject commit a82f3282872e3b033dc636cb60b38a4cefe0002e diff --git a/libxcm/AggregateMessage.cs b/libxcm/AggregateMessage.cs new file mode 100644 index 0000000..2388811 --- /dev/null +++ b/libxcm/AggregateMessage.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace libxcm +{ + internal class AggregateMessage + { + } +} diff --git a/libxcm/CSVConverter.cs b/libxcm/CSVConverter.cs index 8ed12d3..c012580 100644 --- a/libxcm/CSVConverter.cs +++ b/libxcm/CSVConverter.cs @@ -7,9 +7,9 @@ namespace xcmparser { - class CsvConverter : IMessageConverter + class CsvConverter : IMessageFormatter { - public void SendConvertedMessage(libconnection.StreamPipe pipe, Message msg) + public void SendConvertedMessage(libconnection.Pipe pipe, Message msg) { foreach (Symbol symbol in msg) { diff --git a/libxcm/Command.cs b/libxcm/Command.cs index 3999f28..4707bb9 100644 --- a/libxcm/Command.cs +++ b/libxcm/Command.cs @@ -7,11 +7,11 @@ namespace libxcm { public class Command : Message { - public Command(XmlNode commandNode, List knownSymbols, Connection inbound, Connection outbound, string systemName) : base(commandNode, knownSymbols, inbound, outbound, systemName) + public Command(XmlNode commandNode, InboundConnection inbound, OutboundConnection outbound, string systemName) : base(commandNode, inbound, outbound, systemName) { } - protected Command(XmlNode commandNode, List knownSymbols, Func symbolFactory, Func EntryFactory, Connection inbound, Connection outbound) : base(commandNode, knownSymbols, symbolFactory, EntryFactory, inbound, outbound) + protected Command(XmlNode commandNode, Func symbolFactory, Func EntryFactory, InboundConnection inbound, OutboundConnection outbound) : base(commandNode, symbolFactory, EntryFactory, inbound, outbound) { } } diff --git a/libxcm/Connection.cs b/libxcm/Connection.cs deleted file mode 100644 index 7a13914..0000000 --- a/libxcm/Connection.cs +++ /dev/null @@ -1,98 +0,0 @@ -using libconnection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; -using System.Xml; - -namespace libxcm -{ - public class Connection : StreamPipe - { - private readonly object _lock = new (); - public Connection(XmlNode node, bool reverse = false) : base() - { - var iterator = node.GetChildsOrdered(); - if(reverse) - { - iterator = iterator.Reverse(); - } - foreach (XmlNode stage in iterator) - { - if(stage.NodeType != XmlNodeType.Comment) - { - switch(stage.Name.ToLower()) - { - case "stage": - string className = ""; - Dictionary parameters = new (); - foreach (XmlNode parameter in stage) - { - if (stage.Name.ToLower() == "stage") - { - switch(parameter.Name.ToLower()) - { - case "class": - className = parameter.InnerText; - break; - case "parameter": - string parameterName = parameter.Attributes["name"]?.Value; - if(string.IsNullOrWhiteSpace( parameterName ) ) - { - throw new ArgumentException("The connection class parameter must have a name attribute."); - } - parameters.Add(parameterName, parameter.InnerText); - break; - } - } - } - if(className == "") - { - throw new ArgumentException("A connection must have a class element inside it"); - } - var datastream = NameResolver.GetStreamByName(className, parameters); - if (datastream == null) - throw new ArgumentException($"There is no connection class with the name {className}"); - - Add(datastream); - break; - - case "converter": - MessageConverter = GetConverterFromName(stage.InnerText); - break; - } - } - } - } - - public static IMessageConverter GetConverterFromName(string name) - { - switch(name.ToLower()) - { - case "json": - return new xcmparser.JsonConverter(); - case "mqttjson": - return new xcmparser.MqttJsonConverter(); - case "csv": - return new xcmparser.CsvConverter(); - } - throw new ArgumentException($"No message converter with name {name} is available"); - } - - public IMessageConverter MessageConverter { get; set; } = new xcmparser.JsonConverter(); - - public void TransmitParsedData(Message msg) - { - MessageConverter.SendConvertedMessage(this, msg); - } - - public void TransmitParsedDataSynchronized(Message msg) - { - lock(_lock) - { - TransmitParsedData(msg); - } - } - } -} diff --git a/libxcm/IMessageConverter.cs b/libxcm/IMessageFormatter.cs similarity index 61% rename from libxcm/IMessageConverter.cs rename to libxcm/IMessageFormatter.cs index 0ee1a85..d3e1b64 100644 --- a/libxcm/IMessageConverter.cs +++ b/libxcm/IMessageFormatter.cs @@ -7,8 +7,8 @@ namespace libxcm { - public interface IMessageConverter + public interface IMessageFormatter { - public void SendConvertedMessage(StreamPipe pipe, Message msg); + public void SendConvertedMessage(Pipe pipe, Message msg); } } diff --git a/libxcm/ISystemFactory.cs b/libxcm/ISystemFactory.cs new file mode 100644 index 0000000..2f9c84a --- /dev/null +++ b/libxcm/ISystemFactory.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace libxcm +{ + public interface ISystemFactory + { + public XCMSystem BuildSystem(XmlNode node, Dictionary inboundconnections, Dictionary outboundconnections); + public ITokenFactory GetTokenFactory(); + } +} diff --git a/libxcm/ITokenFactory.cs b/libxcm/ITokenFactory.cs new file mode 100644 index 0000000..ae950fa --- /dev/null +++ b/libxcm/ITokenFactory.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace libxcm +{ + public interface ITokenFactory + { + public Message BuildMessage(XmlNode node, InboundConnection inbound, OutboundConnection outbound, string systemName); + public Command BuildCommand(XmlNode node, InboundConnection inbound, OutboundConnection outbound, string systemName); + public Symbol BuildSymbol(XmlNode node); + } +} diff --git a/libxcm/ITokenizerFactory.cs b/libxcm/ITokenizerFactory.cs deleted file mode 100644 index c65618f..0000000 --- a/libxcm/ITokenizerFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml; - -namespace libxcm -{ - public interface ITokenizerFactory - { - XCMTokenizer BuildTokenizer(XmlNode root, Dictionary inboundConnection, Dictionary outboundConnection); - XCMTokenizer BuildTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnection, Dictionary outboundConnection); - } -} diff --git a/libxcm/InboundConnection.cs b/libxcm/InboundConnection.cs new file mode 100644 index 0000000..bea251c --- /dev/null +++ b/libxcm/InboundConnection.cs @@ -0,0 +1,77 @@ +using libconnection; +using libconnection.DataProzessor; +using libconnection.Interfaces; +using libconnection.Packetizer; +using System; +using System.Collections.Generic; +using System.Xml; + +namespace libxcm +{ + public class InboundConnection : Pipe + { + static private readonly MessageConverterNameResolver nameResolver = MessageConverterNameResolver.Instance; + + public static T BuildConnectionPartByNode(XmlElement node) + { + Dictionary parameters = new(); + foreach (XmlNode parameter in node) + { + if (parameter.Name.ToLower() == "parameter") + { + string parameterName = parameter.Attributes["name"]?.Value; + if (string.IsNullOrWhiteSpace(parameterName)) + { + throw new ArgumentException("The connection node must have a name attribute."); + } + parameters.Add(parameterName, parameter.InnerText); + } + } + var className = ((XmlElement)node).GetAttribute("class"); + if (string.IsNullOrWhiteSpace(className)) + { + throw new ArgumentException("Each connection node must have a classname assigned"); + } + if(typeof(T) == typeof(IInterface)) + { + return (T)nameResolver.GetInterfaceByName(className, parameters); + } + else if(typeof(T) == typeof(IPacketizer)) + { + return (T)nameResolver.GetPacketizerByName(className, parameters); + } + else if(typeof(T) == typeof(IDataProcessor)) + { + return (T)nameResolver.GetProcessorByName(className, parameters); + } + else + { + throw new ArgumentException(); + } + } + + public InboundConnection(XmlNode node, IDictionary ifaces) : base() + { + foreach (XmlNode stage in node) + { + if(stage.NodeType != XmlNodeType.Comment) + { + switch(stage.Name.ToLower()) + { + case "interface": + string ifaceName = stage.InnerText; + AddInterface(ifaces[ifaceName]); + break; + case "packetizer": + AddPacketizer(BuildConnectionPartByNode((XmlElement)stage)); + break; + case "processor": + break; + case "filter": + break; + } + } + } + } + } +} diff --git a/libxcm/JsonConverter.cs b/libxcm/JsonConverter.cs index f4f47c5..6c99a57 100644 --- a/libxcm/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -9,7 +9,7 @@ namespace xcmparser { - public class JsonConverter : IMessageConverter + public class JsonConverter : IMessageFormatter { private static object SymbolToTagedObject(Symbol symb) { @@ -107,7 +107,7 @@ public static void GetDataFromJson(string json, Command com) jsoncommand?.FillCommand(com); } - public void SendConvertedMessage(libconnection.StreamPipe pipe, Message msg) + public void SendConvertedMessage(libconnection.Pipe pipe, Message msg) { var data = ConvertToByteArray(msg, out object additionalData); var convertedMsg = new libconnection.Message(data) diff --git a/libxcm/Message.cs b/libxcm/Message.cs index 8c65706..4ffbeb5 100644 --- a/libxcm/Message.cs +++ b/libxcm/Message.cs @@ -12,8 +12,8 @@ public class Message : IEnumerable { protected readonly byte[] identifier; protected readonly List symbols = new List(); - protected readonly Connection inbound; - protected readonly Connection outbound; + protected readonly InboundConnection inbound; + protected readonly OutboundConnection outbound; public int IDOffset { get; protected set; } = 0; private int idlength = -1; @@ -30,12 +30,12 @@ private static Entry EntryFactory(XmlNode entryNode) public string SystemName{get;set;} - public Message(XmlNode messageNode, List knownSymbols, Connection inbound, Connection outbound, string systemName) : this(messageNode, knownSymbols, SymbolFactory, EntryFactory, inbound, outbound) + public Message(XmlNode messageNode, InboundConnection inbound, OutboundConnection outbound, string systemName) : this(messageNode, SymbolFactory, EntryFactory, inbound, outbound) { SystemName = systemName; } - protected Message(XmlNode messageNode, List knownSymbols, Func symbolFactory, Func EntryFactory, Connection inbound, Connection outbound) + protected Message(XmlNode messageNode, Func symbolFactory, Func EntryFactory, InboundConnection inbound, OutboundConnection outbound) { this.inbound = inbound; this.outbound = outbound; @@ -65,6 +65,7 @@ protected Message(XmlNode messageNode, List knownSymbols, Func x.Name == symbolRef) ?? throw new ArgumentException("ref doesn't point to a defined symbol")).Copy(); if (symbolNode.Attributes["name"] != null) @@ -79,7 +80,7 @@ protected Message(XmlNode messageNode, List knownSymbols, Func formatterClasses = new(); + public static new MessageConverterNameResolver Instance + { + get + { + lock (padlock) + { + if (instance == null) + { + instance = new MessageConverterNameResolver(); + return instance; + } + return instance; + } + } + } + + protected MessageConverterNameResolver() : base() + { + LoadOutputFormatters(typeof(MessageConverterNameResolver).Assembly); + } + + public void LoadOutputFormatters(Assembly assy) + { + foreach (var type in assy.GetTypes()) + { + if (type.GetCustomAttribute(typeof(OutputFormatterAttribute)) is OutputFormatterAttribute attribute) + { + if (!formatterClasses.TryAdd(attribute.GetClassName(), type)) + { + var originalType = formatterClasses[attribute.GetClassName()]; + throw new ArgumentException($"Multiple Interfaces with the same name: {attribute.GetClassName()} in {type.FullName} and {originalType.FullName}"); + } + } + } + } + + public IMessageFormatter GeFormatterByName(string name, IDictionary initialization) + { + if (formatterClasses.TryGetValue(name.ToLower(), out Type itype)) + { + var method = itype.GetMethod("GenerateWithParameters"); + if (method != null) + { + return method.Invoke(null, new IDictionary[] { initialization }) as IMessageFormatter; + } + else + { + var constructor = itype.GetConstructor(Type.EmptyTypes); + return constructor.Invoke(null) as IMessageFormatter; + } + } + return null; + } + } +} diff --git a/libxcm/MqttJsonConverter.cs b/libxcm/MqttJsonConverter.cs index ce49e61..8e50008 100644 --- a/libxcm/MqttJsonConverter.cs +++ b/libxcm/MqttJsonConverter.cs @@ -6,39 +6,66 @@ namespace xcmparser { - class MqttJsonConverter : IMessageConverter + [OutputFormatter("mqtt")] + class MqttJsonConverter : IMessageFormatter { + public static MqttJsonConverter GenerateWithParameters(IDictionary parameter) + { + bool splitMessage = true; + if (parameter.ContainsKey("splitmessage")) + { + splitMessage = parameter["splitmessage"].ToLower() == "true"; + } + + return new MqttJsonConverter() + { + SplitMessages = splitMessage + }; + } + public bool SplitMessages { get; set; } = true; private static void AddTag(ref string topic, string tag) { topic += "_" + tag; } - public void SendConvertedMessage(libconnection.StreamPipe pipe, Message msg) + public void SendConvertedMessage(libconnection.Pipe pipe, Message msg) { - foreach (Symbol symbol in msg) + string mqttTopic = msg.Name; + if (!mqttTopic.EndsWith("/")) + { + mqttTopic += "/"; + } + if (SplitMessages) { - foreach (Entry entry in symbol) + foreach (Symbol symbol in msg) { - string mqttTopic = msg.Name; - if(!mqttTopic.EndsWith("/")) + foreach (Entry entry in symbol) { - mqttTopic += "/"; + if (!symbol.IsAnonymous) + { + AddTag(ref mqttTopic, symbol.Name); + } + AddTag(ref mqttTopic, entry.Name); + string json = JsonSerializer.Serialize(new + { + value = entry.GetValue() + }); + pipe.TransmitMessage(new libconnection.Message(json) + { + CustomObject = mqttTopic + }); } - if(!symbol.IsAnonymous) - { - AddTag(ref mqttTopic, symbol.Name); - } - AddTag(ref mqttTopic, entry.Name); - string json = JsonSerializer.Serialize(new - { - value = entry.GetValue() - }); - pipe.TransmitMessage(new libconnection.Message(json) - { - CustomObject = mqttTopic - }); } } + else + { + string messageName = msg.Name; + var bytes = JsonConverter.ConvertDataToJSONByte(msg, ref messageName); + pipe.TransmitMessage(new libconnection.Message(bytes) + { + CustomObject = mqttTopic + }); + } } } } \ No newline at end of file diff --git a/libxcm/OutboundConnection.cs b/libxcm/OutboundConnection.cs new file mode 100644 index 0000000..7d09c80 --- /dev/null +++ b/libxcm/OutboundConnection.cs @@ -0,0 +1,63 @@ +using libconnection; +using libconnection.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace libxcm +{ + public class OutboundConnection : InboundConnection + { + private static readonly MessageConverterNameResolver nameresolver = MessageConverterNameResolver.Instance; + + public static new T BuildConnectionPartByNode(XmlElement node) + { + Dictionary parameters = new(); + foreach (XmlNode parameter in node) + { + if (parameter.Name.ToLower() == "parameter") + { + string parameterName = parameter.Attributes["name"]?.Value; + if (string.IsNullOrWhiteSpace(parameterName)) + { + throw new ArgumentException("The connection node must have a name attribute."); + } + parameters.Add(parameterName, parameter.InnerText); + } + } + var className = ((XmlElement)node).GetAttribute("name"); + if (string.IsNullOrWhiteSpace(className)) + { + throw new ArgumentException("A formatter must be selected with a unique name"); + } + + if (typeof(T) == typeof(IMessageFormatter)) + { + return (T)nameresolver.GeFormatterByName(className, parameters); + } + else + { + return InboundConnection.BuildConnectionPartByNode(node); + } + } + + public OutboundConnection(XmlNode node, IDictionary ifaces) : base(node, ifaces) + { + var outformatNode = node.SelectSingleNode("outputformat"); + if(outformatNode != null) + { + MessageConverter = BuildConnectionPartByNode((XmlElement)outformatNode); + } + } + + public IMessageFormatter MessageConverter { get; set; } = new xcmparser.JsonConverter(); + + public void TransmitParsedData(Message msg) + { + MessageConverter.SendConvertedMessage(this, msg); + } + } +} diff --git a/libxcm/OutputFormatterAttribute.cs b/libxcm/OutputFormatterAttribute.cs new file mode 100644 index 0000000..d643a5f --- /dev/null +++ b/libxcm/OutputFormatterAttribute.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace libxcm +{ + [AttributeUsage(AttributeTargets.Class)] + internal class OutputFormatterAttribute : Attribute + { + private string className; + + public OutputFormatterAttribute(string className) + { + this.className = className; + } + public string GetClassName() + { + return className; + } + } +} diff --git a/libxcm/System.cs b/libxcm/System.cs new file mode 100644 index 0000000..17f00c8 --- /dev/null +++ b/libxcm/System.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace libxcm +{ + public class XCMSystem + { + protected readonly ITokenFactory factory; + protected Dictionary> compiledtokens = new Dictionary>(); + protected List knownSymbols = new List(); + protected InboundConnection inbound; + protected OutboundConnection outbound; + public XCMSystem(XmlNode root, ITokenFactory factory, Dictionary inboundConnections, Dictionary outboundConnections) + { + this.factory = factory; + Name = root.Attributes["name"]?.Value ?? ""; + inbound = GetInboundConnectionFromXML(root, inboundConnections); + outbound = GetOutboundConnectionFromXML(root, outboundConnections); + if (inbound == null || outbound == null) + { + throw new ArgumentException("The system must have one inbound and outbound connection"); + } + foreach (XmlNode token in root) + { + if (token.NodeType != XmlNodeType.Comment) + { + foreach (XmlNode t in token) + { + if (t.NodeType != XmlNodeType.Comment) + { + BuildToken(token, t, Name); + } + } + } + } + } + + void BuildToken(XmlNode parent, XmlNode node, string systemName) + { + switch (node.Name.ToLower()) + { + /* + case "symbol": + if (!compiledtokens.ContainsKey("symbol")) + { + compiledtokens.Add("symbol", new List()); + } + Symbol symb = factory.BuildSymbol(node); + compiledtokens["symbol"].Add(symb); + knownSymbols.Add(symb); + break; + */ + case "message": + if (!compiledtokens.ContainsKey("message")) + { + compiledtokens.Add("message", new List()); + } + compiledtokens["message"].Add(factory.BuildMessage(node, inbound, outbound, systemName)); + break; + case "command": + if (!compiledtokens.ContainsKey("command")) + { + compiledtokens.Add("command", new List()); + } + compiledtokens["command"].Add(factory.BuildCommand(node, inbound, outbound, systemName)); + break; + /* + case "Group": + MessageGroup group = BuildGroup(node); + foreach(Message msg in group.Messages) + { + compiledtokens["message"].Add(msg); + } + break;*/ + default: + break; + } + } + + private static InboundConnection GetInboundConnectionFromXML(XmlNode node, Dictionary connections) + { + var defInput = node.SelectSingleNode("defaultInput"); + if (defInput == null) return null; + var connName = defInput.InnerText; + if (connections.TryGetValue(connName, out InboundConnection value)) return value; + else + { + return null; + } + } + + private static OutboundConnection GetOutboundConnectionFromXML(XmlNode node, Dictionary connections) + { + var defInput = node.SelectSingleNode("defaultOutput"); + if (defInput == null) return null; + var connName = defInput.InnerText; + if (connections.TryGetValue(connName, out OutboundConnection value)) return value; + else + { + return null; + } + } + + public IEnumerable GetObjects(bool strict = true) + { + foreach (var t in compiledtokens) + { + foreach (object obj in t.Value) + { + if (strict) + { + if (obj.GetType() == typeof(T)) + { + yield return (T)obj; + } + } + else + { + if (obj is T val) + { + yield return val; + } + } + } + } + } + + public string Name { get; set; } + } +} diff --git a/libxcm/SystemFactory.cs b/libxcm/SystemFactory.cs new file mode 100644 index 0000000..7e65de4 --- /dev/null +++ b/libxcm/SystemFactory.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace libxcm +{ + public class SystemFactory : ISystemFactory, ITokenFactory + { + private readonly ITokenFactory tokenfactory; + + public SystemFactory() + { + tokenfactory = this; + } + public SystemFactory(ITokenFactory tokenfactory) + { + this.tokenfactory = tokenfactory; + } + + public Command BuildCommand(XmlNode node, InboundConnection inbound, OutboundConnection outbound, string systemName) + { + return new Command(node, inbound, outbound, systemName); + } + + public Message BuildMessage(XmlNode node, InboundConnection inbound, OutboundConnection outbound, string systemName) + { + return new Message(node, inbound, outbound, systemName); + } + + public Symbol BuildSymbol(XmlNode node) + { + return new Symbol(node); + } + + public XCMSystem BuildSystem(XmlNode node, Dictionary inboundconnections, Dictionary outboundconnections) + { + return new XCMSystem(node, tokenfactory, inboundconnections, outboundconnections); + } + + public ITokenFactory GetTokenFactory() + { + return tokenfactory; + } + } +} diff --git a/libxcm/XCMDokument.cs b/libxcm/XCMDokument.cs index 52f2fc9..948e494 100644 --- a/libxcm/XCMDokument.cs +++ b/libxcm/XCMDokument.cs @@ -1,4 +1,6 @@ -using System; +using libconnection; +using libconnection.Interfaces; +using System; using System.Collections.Generic; using System.Text; using System.Threading; @@ -9,19 +11,37 @@ namespace libxcm { public class XCMDokument { - private readonly List symbols = new (); - private readonly Dictionary tokenizers = new (); + private readonly Dictionary systems = new (); - private readonly Dictionary incommingConnections = new(); - private readonly Dictionary outgoingConnections = new(); - public XCMDokument(XmlDocument doc, ITokenizerFactory factory) : this(doc.DocumentElement, factory) + private readonly Dictionary incommingConnections = new(); + private readonly Dictionary outgoingConnections = new(); + + private readonly Dictionary interfaces = new (); + + + public XCMDokument(XmlDocument doc, ISystemFactory factory) : this(doc.DocumentElement, factory) { } - public XCMDokument(XmlElement root, ITokenizerFactory factory) + public XCMDokument(XmlElement root, ISystemFactory factory) { - foreach (XmlElement incon in root.SelectNodes("/spacesystem/connections/incomming/connection")) //Read all incomming connections and generate connection elements from it + foreach (XmlElement ifaceNode in root.SelectNodes("/systems/interfaces/interface")) + { + var ifaceName = ifaceNode.GetAttribute("name"); + if (string.IsNullOrWhiteSpace(ifaceName)) + { + throw new ArgumentException("Each interface element must have a name"); + } + if (interfaces.ContainsKey(ifaceName)) + { + throw new ArgumentException("Each interface element must have a unique name"); + } + interfaces.Add(ifaceName, InboundConnection.BuildConnectionPartByNode(ifaceNode)); + } + + + foreach (XmlElement incon in root.SelectNodes("/systems/connections/incomming/connection")) //Read all incomming connections and generate connection elements from it { string connName = incon.GetAttribute("name"); if(string.IsNullOrWhiteSpace(connName)) @@ -32,11 +52,11 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory) { throw new ArgumentException("Each connection element must have a unique name"); } - var connection = new Connection(incon); + var connection = new InboundConnection(incon, interfaces); incommingConnections.Add(connName, connection); } - foreach (XmlElement outcon in root.SelectNodes("/spacesystem/connections/outgoing/connection")) //Read all outgoing connections and generate connection elements from it + foreach (XmlElement outcon in root.SelectNodes("/systems/connections/outgoing/connection")) //Read all outgoing connections and generate connection elements from it { string connName = outcon.GetAttribute("name"); if (string.IsNullOrWhiteSpace(connName)) @@ -47,23 +67,17 @@ public XCMDokument(XmlElement root, ITokenizerFactory factory) { throw new ArgumentException("Each connection element must have a unique name"); } - var connection = new Connection(outcon, true); + var connection = new OutboundConnection(outcon, interfaces); outgoingConnections.Add(connName, connection); } - foreach(XmlElement globalSymbol in root.SelectNodes("/spacesystem/symbols/symbol")) - { - - } - - foreach (XmlElement systemNode in root.SelectNodes("/spacesystem/system")) + foreach (XmlElement systemNode in root.SelectNodes("/systems/system")) { string systemname = systemNode.Attributes["name"].Value; - if (systemname == null || tokenizers.ContainsKey(systemname)) + if (systemname == null || systems.ContainsKey(systemname)) throw new ArgumentException("A system must have a unique name"); - var tokenizer = factory.BuildTokenizer(systemNode, symbols, incommingConnections, outgoingConnections); - tokenizer.Name = systemname; - tokenizers.Add(systemname, tokenizer); + var system = factory.BuildSystem(systemNode, incommingConnections, outgoingConnections); + systems.Add(systemname, system); } } @@ -72,24 +86,24 @@ public async Task StartConnectionsAsync(CancellationToken token) List tasks = new(); foreach(var connection in incommingConnections.Values) { - tasks.Add(connection.StartStreamAsync(token)); + tasks.Add(connection.StartPipeAsync(token)); } foreach (var connection in outgoingConnections.Values) { - tasks.Add(connection.StartStreamAsync(token)); + tasks.Add(connection.StartPipeAsync(token)); } await Task.WhenAll(tasks); } - public IEnumerable GetTokenizers() + public IEnumerable GetTokenizers() { - return tokenizers.Values; + return systems.Values; } - public XCMTokenizer GetTokenzierByName(string name) + public XCMSystem GetTokenzierByName(string name) { - return tokenizers[name]; + return systems[name]; } } } diff --git a/libxcm/XCMFactory.cs b/libxcm/XCMFactory.cs deleted file mode 100644 index 3c3333d..0000000 --- a/libxcm/XCMFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml; - -namespace libxcm -{ - public class XCMFactory : ITokenizerFactory - { - public virtual XCMTokenizer BuildTokenizer(XmlNode root, Dictionary inboundConnection, Dictionary outboundConnection) - { - return BuildTokenizer(root, null, inboundConnection, outboundConnection); - } - - public XCMTokenizer BuildTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnection, Dictionary outboundConnection) - { - return new XCMTokenizer(root, symbols, inboundConnection, outboundConnection); - } - } -} diff --git a/libxcm/XCMTokenizer.cs b/libxcm/XCMTokenizer.cs deleted file mode 100644 index fdb2db0..0000000 --- a/libxcm/XCMTokenizer.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml; -using System.Linq; -using System.Xml.Linq; - -namespace libxcm -{ - public class XCMTokenizer - { - protected Dictionary> compiledtokens = new Dictionary>(); - protected List knownSymbols = new List(); - protected Connection inbound; - protected Connection outbound; - - public XCMTokenizer(XmlNode root, IEnumerable symbols, Connection inboundConnection, Connection outboundConnection) - { - if (inboundConnection == null || outboundConnection == null) - { - throw new ArgumentException("Each system must have a default input and output connection"); - } - - inbound = inboundConnection; - outbound = outboundConnection; - - if (symbols != null) - { - AddKownSymbols(symbols); - } - string systenName = root.Attributes["name"]?.Value ?? ""; - foreach (XmlNode token in root) - { - if (token.NodeType != XmlNodeType.Comment) - { - foreach (XmlNode t in token) - { - if (t.NodeType != XmlNodeType.Comment) - { - BuildToken(token, t, systenName); - } - } - } - } - } - public XCMTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnections, Dictionary outboundConnections) : this(root, symbols, GetInboundConnectionFromXML(root, inboundConnections), GetOutboundConnectionFromXML(root, outboundConnections)) - { - } - - public string Name { get; set; } = string.Empty; - - private static Connection GetInboundConnectionFromXML(XmlNode node, Dictionary connections) - { - var defInput = node.SelectSingleNode("defaultInput"); - if(defInput == null) return null; - var connName = defInput.InnerText; - if(connections.ContainsKey(connName)) return connections[connName]; - else - { - return null; - } - } - - private static Connection GetOutboundConnectionFromXML(XmlNode node, Dictionary connections) - { - var defInput = node.SelectSingleNode("defaultOutput"); - if (defInput == null) return null; - var connName = defInput.InnerText; - if (connections.ContainsKey(connName)) return connections[connName]; - else - { - return null; - } - } - - public IEnumerable GetObjects(bool strict = true) - { - foreach(var t in compiledtokens) - { - foreach(object obj in t.Value) - { - if (strict) - { - if (obj.GetType() == typeof(T)) - { - yield return (T)obj; - } - } - else - { - if (obj is T val) - { - yield return val; - } - } - } - } - } - - virtual protected void AddKownSymbols(IEnumerable symbols) - { - foreach(var symb in symbols) - { - knownSymbols.Add(symb.Copy()); - } - } - - virtual public Message BuildMessage(XmlNode node, Connection inbound, Connection outbound, string systemName) - { - return new Message(node, knownSymbols, inbound, outbound, systemName); - } - - virtual public Command BuildCommand(XmlNode node, Connection inbound, Connection outbound, string systemName) - { - return new Command(node, knownSymbols, inbound, outbound, systemName); - } - - virtual public Symbol BuildSymbol(XmlNode node) - { - return new Symbol(node); - } - - virtual public MessageGroup BuildGroup(XmlNode node) - { - throw new NotImplementedException(); - //return new MessageGroup(node, knownSymbols, this); - } - virtual protected void BuildToken(XmlNode parent, XmlNode node, string systemName) - { - switch(node.Name.ToLower()) - { - case "symbol": - if(!compiledtokens.ContainsKey("symbol")) - { - compiledtokens.Add("symbol", new List()); - } - Symbol symb = BuildSymbol(node); - compiledtokens["symbol"].Add(symb); - knownSymbols.Add(symb); - break; - case "message": - if (!compiledtokens.ContainsKey("message")) - { - compiledtokens.Add("message", new List()); - } - compiledtokens["message"].Add(BuildMessage(node, inbound, outbound, systemName)); - break; - case "command": - if (!compiledtokens.ContainsKey("command")) - { - compiledtokens.Add("command", new List()); - } - compiledtokens["command"].Add(BuildCommand(node, inbound, outbound, systemName)); - break; - /* - case "Group": - MessageGroup group = BuildGroup(node); - foreach(Message msg in group.Messages) - { - compiledtokens["message"].Add(msg); - } - break;*/ - default: - break; - } - } - } -} diff --git a/libxcmparse/DataObjects/DataCommand.cs b/libxcmparse/DataObjects/DataCommand.cs index 7331c44..f688038 100644 --- a/libxcmparse/DataObjects/DataCommand.cs +++ b/libxcmparse/DataObjects/DataCommand.cs @@ -9,12 +9,12 @@ namespace libxcmparse.DataObjects { public class DataCommand : Command { - public DataCommand(XmlNode commandNode, List knownSymbols, Connection inbound, Connection outbound, string systemName) : this(commandNode, knownSymbols, DataMessage.SymbolFactory, DataMessage.EntryFactory, inbound, outbound) + public DataCommand(XmlNode commandNode, InboundConnection inbound, OutboundConnection outbound, string systemName) : this(commandNode, DataMessage.SymbolFactory, DataMessage.EntryFactory, inbound, outbound) { SystemName = systemName; } - protected DataCommand(XmlNode commandNode, List knownSymbols, Func symbolFactory, Func EntryFactory, Connection inbound, Connection outbound) : base(commandNode, knownSymbols, symbolFactory, EntryFactory, inbound, outbound) + protected DataCommand(XmlNode commandNode, Func symbolFactory, Func EntryFactory, InboundConnection inbound, OutboundConnection outbound) : base(commandNode, symbolFactory, EntryFactory, inbound, outbound) { List sibblings = new List(); foreach (DataSymbol symb in this) @@ -29,7 +29,7 @@ protected DataCommand(XmlNode commandNode, List knownSymbols, Func knownSymbols, Connection inboundConnection, Connection outboundConnection, string systemName) : base(messageNode, knownSymbols, SymbolFactory, EntryFactory, inboundConnection, outboundConnection) + public DataMessage(XmlNode messageNode, InboundConnection inboundConnection, OutboundConnection outboundConnection, string systemName) : base(messageNode, SymbolFactory, EntryFactory, inboundConnection, outboundConnection) { SystemName = systemName; List sibblings = new List(); @@ -36,7 +36,7 @@ public DataMessage(XmlNode messageNode, List knownSymbols, Connection in inbound.MessageReceived += Inbound_MessageReceived; } - private void Inbound_MessageReceived(object sender, libconnection.MessageEventArgs e) + private void Inbound_MessageReceived(object sender, libconnection.MessageReceivedEventArgs e) { var msg = e.Message; if (Match(msg)) diff --git a/libxcmparse/DataTokenFactory.cs b/libxcmparse/DataTokenFactory.cs new file mode 100644 index 0000000..6b10904 --- /dev/null +++ b/libxcmparse/DataTokenFactory.cs @@ -0,0 +1,29 @@ +using libxcm; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using libxcmparse.DataObjects; + +namespace libxcmparse +{ + public class DataTokenFactory : ITokenFactory + { + public Command BuildCommand(XmlNode node, InboundConnection inbound, OutboundConnection outbound, string systemName) + { + return new DataCommand(node, inbound, outbound, systemName); + } + + public Message BuildMessage(XmlNode node, InboundConnection inbound, OutboundConnection outbound, string systemName) + { + return new DataMessage(node, inbound, outbound, systemName); + } + + public Symbol BuildSymbol(XmlNode node) + { + return new DataSymbol(node); + } + } +} diff --git a/libxcmparse/ISerializer.cs b/libxcmparse/ISerializer.cs deleted file mode 100644 index c3d2405..0000000 --- a/libxcmparse/ISerializer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using libxcm; -using System; -using System.Collections.Generic; -using System.Text; - -namespace libxcmparse -{ - public interface ISerializer - { - T Serialize(XCMTokenizer tokenizer); - T Serialize(Message message); - T Serialize(Symbol symbol); - T Serialize(Entry entry); - object GetDataObject(Message message); - object GetDataObject(Symbol symbol); - object GetDataObject(Entry entry); - } -} diff --git a/libxcmparse/RecieveDataHandler.cs b/libxcmparse/RecieveDataHandler.cs deleted file mode 100644 index ad533d9..0000000 --- a/libxcmparse/RecieveDataHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using libxcmparse.DataObjects; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using xcmparser; - -namespace libxcmparse -{ - public class RecieveDataHandler - { - private List dataMessages = new List(); - - public RecieveDataHandler(XCMParserTokenizer tokenizer) - { - dataMessages.AddRange(tokenizer.GetObjects()); - Tokenizer = tokenizer; - } - - public XCMParserTokenizer Tokenizer { get; } - - public bool ReceiveData(IEnumerable data) - { - bool hasmatched = false; - foreach(var msg in dataMessages) - { - if(msg.Match(data)) - { - msg.ParseMessage(data.Skip(msg.IDByteLength + msg.IDOffset)); - hasmatched = true; - } - } - return hasmatched; - } - } -} diff --git a/libxcmparse/XCMParserTokenizer.cs b/libxcmparse/XCMParserTokenizer.cs deleted file mode 100644 index a3bb1b9..0000000 --- a/libxcmparse/XCMParserTokenizer.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml; -using libxcm; -using libxcmparse.DataObjects; - -namespace xcmparser -{ - public class XCMParserTokenizer : XCMTokenizer - { - public XCMParserTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnection, Dictionary outboundConnection) : base(root, symbols, inboundConnection, outboundConnection) - { - foreach(var s in symbols) - { - if(s is not DataSymbol) - { - throw new ArgumentException("The parser tokenizer only supports datasymbols"); - } - } - } - - public override Symbol BuildSymbol(XmlNode node) - { - return new DataSymbol(node); - } - - public override Command BuildCommand(XmlNode node, Connection inbound, Connection outbound, string systemname) - { - return new DataCommand(node, knownSymbols, inbound, outbound, systemname); - } - - public override Message BuildMessage(XmlNode node, Connection inbound, Connection outbound, string systemname) - { - return new DataMessage(node, knownSymbols, inbound, outbound, systemname); - } - } -} diff --git a/libxcmparse/XCMParserTokenizerFactory.cs b/libxcmparse/XCMParserTokenizerFactory.cs deleted file mode 100644 index 05b83b4..0000000 --- a/libxcmparse/XCMParserTokenizerFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using libxcm; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using xcmparser; - -namespace libxcmparse -{ - public class XCMParserTokenizerFactory : ITokenizerFactory - { - public XCMTokenizer BuildTokenizer(XmlNode root, Dictionary inboundConnection, Dictionary outboundConnection) - { - return BuildTokenizer(root, null, inboundConnection, outboundConnection); - } - - public XCMTokenizer BuildTokenizer(XmlNode root, IEnumerable symbols, Dictionary inboundConnection, Dictionary outboundConnection) - { - return new XCMParserTokenizer(root, symbols, inboundConnection, outboundConnection); - } - } -} diff --git a/xcmparser/FMSXCM.xml b/xcmparser/FMSXCM.xml new file mode 100644 index 0000000..faab310 --- /dev/null +++ b/xcmparser/FMSXCM.xml @@ -0,0 +1,94 @@ + + + + + COM4 + 115200 + + + 4a7ebbedfcbc4e93bc096a285b47bee3.s2.eu.hivemq.cloud:8883 + AC1 + true + groundstation + Pick1234 + + + + + + serialConnection + + + + + + + primaryOutput + + false + + + + + + serialSMP + defaultOut + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 4489d75..20c1731 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -35,7 +35,7 @@ class Options [Option("enableinterface", HelpText = "Enables a basic text interface that can be opened by pressing the t key in the console window")] public bool EnableInterace { get; set; } = false; [Option("websocket", HelpText = "Enables the websocket interface on the specified IPEndpoint eg: 127.0.0.1:9001")] - public string websocketAddress { get; set; } = string.Empty; + public string WebsocketAddress { get; set; } = string.Empty; } class Program { @@ -78,8 +78,8 @@ static async Task Main(string[] args) return -1; } - var tokenizerFactory = new XCMParserTokenizerFactory(); - var xcmdoc = new XCMDokument(doc, tokenizerFactory); + var systemfactory = new SystemFactory(new DataTokenFactory()); + var xcmdoc = new XCMDokument(doc, systemfactory); await xcmdoc.StartConnectionsAsync(cts.Token); } @@ -105,8 +105,8 @@ enum State EditCommand, EditEntry } - - static void CommandEditStateMachine(XCMParserTokenizer tokenizer, DataStream stream) +/* + static void CommandEditStateMachine(XCMParserTokenizer tokenizer, Pipe stream) { State state = State.ShowInfo; bool running = true; @@ -259,8 +259,7 @@ static IEnumerable> GetCommandEntries(DataCo } } } - - static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, DataStream stream, out string jsondata, Options options = null) + static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, Pipe stream, out string jsondata, Options options = null) { bool ret = false; jsondata = string.Empty; @@ -288,5 +287,6 @@ static bool ProcessMessage(IEnumerable data, XCMParserTokenizer tokenizer, } return ret; } +*/ } } From f2d733d2d732633a30c5c1c2526a603f6a1bda41 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Fri, 7 Jun 2024 00:37:06 +0200 Subject: [PATCH 25/27] Add message receiption for mqtt --- libconnection | 2 +- libxcm/IMessageFormatter.cs | 1 + libxcm/MqttJsonConverter.cs | 8 +++++++- xcmparser/FMSXCM.xml | 17 +++++------------ xcmparser/SampleJsonCommand.txt | 15 +++++++++++++++ 5 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 xcmparser/SampleJsonCommand.txt diff --git a/libconnection b/libconnection index a82f328..772d6d8 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit a82f3282872e3b033dc636cb60b38a4cefe0002e +Subproject commit 772d6d87218eb901e0f2de098faecd7305c3435c diff --git a/libxcm/IMessageFormatter.cs b/libxcm/IMessageFormatter.cs index d3e1b64..9ab24d6 100644 --- a/libxcm/IMessageFormatter.cs +++ b/libxcm/IMessageFormatter.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using libconnection; +using libconnection.Packetizer; namespace libxcm { diff --git a/libxcm/MqttJsonConverter.cs b/libxcm/MqttJsonConverter.cs index 8e50008..9ffc208 100644 --- a/libxcm/MqttJsonConverter.cs +++ b/libxcm/MqttJsonConverter.cs @@ -3,6 +3,7 @@ using System.Text.Json; using libxcm.JsonTypes; using System.Collections.Generic; +using libconnection; namespace xcmparser { @@ -28,7 +29,7 @@ private static void AddTag(ref string topic, string tag) topic += "_" + tag; } - public void SendConvertedMessage(libconnection.Pipe pipe, Message msg) + public void SendConvertedMessage(libconnection.Pipe pipe, libxcm.Message msg) { string mqttTopic = msg.Name; if (!mqttTopic.EndsWith("/")) @@ -67,5 +68,10 @@ public void SendConvertedMessage(libconnection.Pipe pipe, Message msg) }); } } + + public IEnumerable Decode(IEnumerable data) + { + throw new System.NotImplementedException(); + } } } \ No newline at end of file diff --git a/xcmparser/FMSXCM.xml b/xcmparser/FMSXCM.xml index faab310..044e444 100644 --- a/xcmparser/FMSXCM.xml +++ b/xcmparser/FMSXCM.xml @@ -1,9 +1,9 @@  - - COM4 - 115200 + + test + transmitTest 4a7ebbedfcbc4e93bc096a285b47bee3.s2.eu.hivemq.cloud:8883 @@ -17,8 +17,6 @@ serialConnection - - @@ -79,14 +77,9 @@ - + - - - - - - + diff --git a/xcmparser/SampleJsonCommand.txt b/xcmparser/SampleJsonCommand.txt new file mode 100644 index 0000000..27eafee --- /dev/null +++ b/xcmparser/SampleJsonCommand.txt @@ -0,0 +1,15 @@ +{ + "Type": "sendcommand", + "Name": "Testcommand", + "Fields": [ + { + "Name": "anonymous", + "Elements": [ + { + "Name": "test", + "value": true + } + ] + } +] +} \ No newline at end of file From 1301aa6f9e38877f858585d833282643309ed615 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Tue, 11 Jun 2024 00:17:49 +0200 Subject: [PATCH 26/27] Begin adding basic logging functions --- libxcmparse/DataObjects/DataMessage.cs | 1 + xcmparser/FMSXCM.xml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libxcmparse/DataObjects/DataMessage.cs b/libxcmparse/DataObjects/DataMessage.cs index 82d9782..5528257 100644 --- a/libxcmparse/DataObjects/DataMessage.cs +++ b/libxcmparse/DataObjects/DataMessage.cs @@ -41,6 +41,7 @@ private void Inbound_MessageReceived(object sender, libconnection.MessageReceive var msg = e.Message; if (Match(msg)) { + Console.WriteLine($"Message {this.Name} received"); ParseMessage(msg); } } diff --git a/xcmparser/FMSXCM.xml b/xcmparser/FMSXCM.xml index 044e444..ddb7143 100644 --- a/xcmparser/FMSXCM.xml +++ b/xcmparser/FMSXCM.xml @@ -1,9 +1,8 @@  - - test - transmitTest + + COM4 4a7ebbedfcbc4e93bc096a285b47bee3.s2.eu.hivemq.cloud:8883 @@ -17,6 +16,7 @@ serialConnection + From 42aa2cf3d857cf3f156875c50d2c2419387dcde8 Mon Sep 17 00:00:00 2001 From: Peter Kremsner Date: Fri, 21 Jun 2024 00:27:43 +0200 Subject: [PATCH 27/27] Enable additional features Adds a feature to replace marked parameters in xml with command line arguments Adds possibility to send additional meta data in the output data stream. This depends on the used output data formatter --- libconnection | 2 +- libxcm/JsonConverter.cs | 12 ++++++------ libxcm/MqttJsonConverter.cs | 13 +++++++------ xcmparser/FMSXCM.xml | 10 +++++----- xcmparser/KeyValuePairConverter.cs | 12 ++++++++++++ xcmparser/Program.cs | 23 ++++++++--------------- xcmparser/xcmparser.csproj | 2 +- 7 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 xcmparser/KeyValuePairConverter.cs diff --git a/libconnection b/libconnection index 772d6d8..77d6ef6 160000 --- a/libconnection +++ b/libconnection @@ -1 +1 @@ -Subproject commit 772d6d87218eb901e0f2de098faecd7305c3435c +Subproject commit 77d6ef6b6614b7f68a4b63fe69d0f024f09a6380 diff --git a/libxcm/JsonConverter.cs b/libxcm/JsonConverter.cs index 6c99a57..7ca1858 100644 --- a/libxcm/JsonConverter.cs +++ b/libxcm/JsonConverter.cs @@ -28,9 +28,9 @@ private static object SymbolToTagedObject(Symbol symb) }; } - public static byte[] ConvertDataToJSONByte(Message msg, ref string msgName) + public static byte[] ConvertDataToJSONByte(Message msg, ref string msgName, IDictionary additionalFields) { - return Encoding.UTF8.GetBytes(ConvertDataToJSON(msg, ref msgName)); + return Encoding.UTF8.GetBytes(ConvertDataToJSON(msg, ref msgName, additionalFields)); } public static object EntryToExtendedJSON(Entry entry) @@ -40,7 +40,7 @@ public static object EntryToExtendedJSON(Entry entry) value = entry.GetValue() }; } - public static string ConvertDataToJSON(Message msg, ref string msgName, bool pretty = false) + public static string ConvertDataToJSON(Message msg, ref string msgName, IDictionary additionalFields, bool pretty = false) { Dictionary tags = new Dictionary(); foreach (Symbol symbol in msg) @@ -68,21 +68,21 @@ public static string ConvertDataToJSON(Message msg, ref string msgName, bool pre Type = "receiveddata", MessageName = msg.Name, Fields = tags, - isExtended = true + Custom = additionalFields }, options) + "\n"; } public byte[] ConvertToByteArray(Message msg) { string dummy = string.Empty; - return ConvertDataToJSONByte(msg, ref dummy); + return ConvertDataToJSONByte(msg, ref dummy, null); } public byte[] ConvertToByteArray(Message msg, out object additionalData) { string name = string.Empty; additionalData = null; - var output = ConvertDataToJSONByte(msg, ref name); + var output = ConvertDataToJSONByte(msg, ref name, null); if (!string.IsNullOrWhiteSpace(name)) { additionalData = name; diff --git a/libxcm/MqttJsonConverter.cs b/libxcm/MqttJsonConverter.cs index 9ffc208..783846a 100644 --- a/libxcm/MqttJsonConverter.cs +++ b/libxcm/MqttJsonConverter.cs @@ -4,6 +4,7 @@ using libxcm.JsonTypes; using System.Collections.Generic; using libconnection; +using System.Linq; namespace xcmparser { @@ -18,12 +19,16 @@ public static MqttJsonConverter GenerateWithParameters(IDictionary x.Key.StartsWith("tag-")).ToDictionary(dict => dict.Key.Split("-").Last(), dict => dict.Value); + return new MqttJsonConverter() { SplitMessages = splitMessage + , Tags = tags }; } public bool SplitMessages { get; set; } = true; + public Dictionary Tags { get; set; } = new(); private static void AddTag(ref string topic, string tag) { topic += "_" + tag; @@ -31,11 +36,7 @@ private static void AddTag(ref string topic, string tag) public void SendConvertedMessage(libconnection.Pipe pipe, libxcm.Message msg) { - string mqttTopic = msg.Name; - if (!mqttTopic.EndsWith("/")) - { - mqttTopic += "/"; - } + string mqttTopic = $"{msg.SystemName}/{msg.Name}"; if (SplitMessages) { foreach (Symbol symbol in msg) @@ -61,7 +62,7 @@ public void SendConvertedMessage(libconnection.Pipe pipe, libxcm.Message msg) else { string messageName = msg.Name; - var bytes = JsonConverter.ConvertDataToJSONByte(msg, ref messageName); + var bytes = JsonConverter.ConvertDataToJSONByte(msg, ref messageName, Tags); pipe.TransmitMessage(new libconnection.Message(bytes) { CustomObject = mqttTopic diff --git a/xcmparser/FMSXCM.xml b/xcmparser/FMSXCM.xml index ddb7143..2ba33ba 100644 --- a/xcmparser/FMSXCM.xml +++ b/xcmparser/FMSXCM.xml @@ -5,9 +5,9 @@ COM4 - 4a7ebbedfcbc4e93bc096a285b47bee3.s2.eu.hivemq.cloud:8883 + 93.177.65.195:1883 AC1 - true + false groundstation Pick1234 @@ -24,6 +24,7 @@ primaryOutput false + {flightnumber} @@ -32,10 +33,9 @@ serialSMP defaultOut - + - - + diff --git a/xcmparser/KeyValuePairConverter.cs b/xcmparser/KeyValuePairConverter.cs new file mode 100644 index 0000000..85c90ed --- /dev/null +++ b/xcmparser/KeyValuePairConverter.cs @@ -0,0 +1,12 @@ +using CommandLine; +using System; +using System.Collections.Generic; +using System.Linq; + +public class KeyValuePairConverter +{ + public static Dictionary Convert(IEnumerable keyValuePairs) + { + return keyValuePairs.Select(part => part.Split('=')).ToDictionary(split => split[0], split => split[1]); + } +} \ No newline at end of file diff --git a/xcmparser/Program.cs b/xcmparser/Program.cs index 20c1731..0edb308 100644 --- a/xcmparser/Program.cs +++ b/xcmparser/Program.cs @@ -16,6 +16,7 @@ using libconnection.Interfaces.UDP; using System.Threading.Tasks; using System.Reflection; +using System.Text.RegularExpressions; namespace xcmparser { @@ -23,19 +24,8 @@ class Options { [Option('f', "xcmfile", Required = true, HelpText = "The xcm file with the package definitions")] public string Xcmfile { get; set; } - [Option('p', "pipe", HelpText = "The pipe expression on how to receive the data")] - public string Pipe { get; set; } - [Option('o', "outputinterface", HelpText = "Interface definition for the output UDP Server eg: 127.0.0.1:9000", Default = "127.0.0.1:9000")] - public string OutputInterface { get; set; } - - [Option('v', "verbose", HelpText = "Enables console data output")] - public bool Verbose { get; set; } = false; - [Option("noforward", HelpText = "Disables the json output except console output")] - public bool NoForward { get; set; } = false; - [Option("enableinterface", HelpText = "Enables a basic text interface that can be opened by pressing the t key in the console window")] - public bool EnableInterace { get; set; } = false; - [Option("websocket", HelpText = "Enables the websocket interface on the specified IPEndpoint eg: 127.0.0.1:9001")] - public string WebsocketAddress { get; set; } = string.Empty; + [Option] + public IEnumerable Replace { get; set; } } class Program { @@ -65,11 +55,14 @@ static async Task Main(string[] args) settings.Schemas.Add("http://www.w3schools.com/", Path.Combine(assemblyDir, "xcm.xsd")); settings.ValidationType = ValidationType.Schema; - XmlReader reader = XmlReader.Create(opts.Xcmfile); + var rawxcm = File.ReadAllText(opts.Xcmfile); + var parameters = KeyValuePairConverter.Convert(opts.Replace); + var replacedxcm = Regex.Replace(rawxcm, @"\{(.+?)\}", m => parameters[m.Groups[1].Value]); + XmlDocument doc = new(); try { - doc.Load(reader); + doc.LoadXml(replacedxcm); } catch (XmlSchemaValidationException ex) { diff --git a/xcmparser/xcmparser.csproj b/xcmparser/xcmparser.csproj index 6ed69f6..1ab7364 100644 --- a/xcmparser/xcmparser.csproj +++ b/xcmparser/xcmparser.csproj @@ -12,7 +12,7 @@ - +