From 6b355a0b72590d3572b706e9db78426b2960fff1 Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 11:26:04 +0200 Subject: [PATCH 01/16] added resistance editing from client to vr engine --- ClientApp/Utils/Client.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ClientApp/Utils/Client.cs b/ClientApp/Utils/Client.cs index 69f9f1b..cef78ab 100644 --- a/ClientApp/Utils/Client.cs +++ b/ClientApp/Utils/Client.cs @@ -155,7 +155,9 @@ namespace ClientApp.Utils } else { - this.handler.setResistance(DataParser.getResistanceFromJson(payloadbytes)); + float resistance = DataParser.getResistanceFromJson(payloadbytes); + this.handler.setResistance(resistance); + engineConnection.BikeResistance = resistance; sendMessage(DataParser.getSetResistanceResponseJson(true)); } break; From 63b5c6ab73c4a2535579ea6f290b013e7bda4d0c Mon Sep 17 00:00:00 2001 From: shinichi Date: Mon, 19 Oct 2020 11:26:09 +0200 Subject: [PATCH 02/16] removed unnecessary code --- DoctorApp/ViewModels/ClientInfoViewModel.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/DoctorApp/ViewModels/ClientInfoViewModel.cs b/DoctorApp/ViewModels/ClientInfoViewModel.cs index 83517b1..eee0456 100644 --- a/DoctorApp/ViewModels/ClientInfoViewModel.cs +++ b/DoctorApp/ViewModels/ClientInfoViewModel.cs @@ -16,7 +16,6 @@ namespace DoctorApp.ViewModels class ClientInfoViewModel : ObservableObject { public PatientInfo PatientInfo { get; set; } - public ObservableCollection ChatLog { get; set; } public ICommand StartSession { get; set; } @@ -38,28 +37,24 @@ namespace DoctorApp.ViewModels MainWindowViewModel = mainWindowViewModel; this.PatientInfo = new PatientInfo() { Username = username, Status = "Waiting to start" }; PatientInfo.ChatLog = new ObservableCollection(); - ChatLog = new ObservableCollection(); client = mainWindowViewModel.client; StartSession = new RelayCommand(() => { client.sendMessage(DataParser.getStartSessionJson(PatientInfo.Username)); PatientInfo.Status = "Session started"; - System.Diagnostics.Debug.WriteLine("patient info status" + PatientInfo.Status); }); StopSession = new RelayCommand(() => { client.sendMessage(DataParser.getStopSessionJson(PatientInfo.Username)); PatientInfo.Status = "Session stopped, waiting to start again."; - System.Diagnostics.Debug.WriteLine("patient info status" + PatientInfo.Status); }); Chat = new RelayCommand((parameter) => { client.sendMessage(DataParser.getChatJson(PatientInfo.Username, ((TextBox)parameter).Text)); PatientInfo.ChatLog.Add(DateTime.Now + ": " + ((TextBox)parameter).Text); - ChatLog.Add(DateTime.Now + ":derp: " + ((TextBox)parameter).Text); }); //TODO RelayCommand ChatToAll From 163d2321cc9fd76178cc2d3a213ae250318cd5ef Mon Sep 17 00:00:00 2001 From: shinichi Date: Mon, 19 Oct 2020 11:49:34 +0200 Subject: [PATCH 03/16] username back added to tabs --- DoctorApp/App.xaml | 4 ++-- DoctorApp/Utils/Client.cs | 1 - DoctorApp/ViewModels/MainViewModel.cs | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/DoctorApp/App.xaml b/DoctorApp/App.xaml index aac18d8..86b2dd4 100644 --- a/DoctorApp/App.xaml +++ b/DoctorApp/App.xaml @@ -9,11 +9,11 @@ - + - + diff --git a/DoctorApp/Utils/Client.cs b/DoctorApp/Utils/Client.cs index 4e727d0..f5fa102 100644 --- a/DoctorApp/Utils/Client.cs +++ b/DoctorApp/Utils/Client.cs @@ -111,7 +111,6 @@ namespace DoctorApp.Utils Console.WriteLine("Set resistance identifier"); break; case DataParser.NEW_CONNECTION: - Debug.WriteLine("doctor client new connection"); this.MainViewModel.NewConnectedUser(DataParser.getUsernameFromResponseJson(payloadbytes)); break; case DataParser.DISCONNECT: diff --git a/DoctorApp/ViewModels/MainViewModel.cs b/DoctorApp/ViewModels/MainViewModel.cs index b12d8dd..bd02499 100644 --- a/DoctorApp/ViewModels/MainViewModel.cs +++ b/DoctorApp/ViewModels/MainViewModel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics; using System.Text; using System.Windows.Controls; using Util; @@ -26,6 +27,7 @@ namespace DoctorApp.ViewModels public void NewConnectedUser(string username) { + Debug.WriteLine("new tab with name " + username); App.Current.Dispatcher.Invoke((Action)delegate { Tabs.Add(new ClientInfoViewModel(MainWindowViewModel, username)); From 17a17e3c6de4b73378cf62ef3b5bcf4669cad13b Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 11:55:20 +0200 Subject: [PATCH 04/16] added comments to dataparser --- Hashing/DataParser.cs | 104 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/Hashing/DataParser.cs b/Hashing/DataParser.cs index 20851f9..a3d7fc4 100644 --- a/Hashing/DataParser.cs +++ b/Hashing/DataParser.cs @@ -39,7 +39,11 @@ namespace Util return Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(json)); } - + /// + /// converts the given string parameter into a message using our protocol. + /// + /// the message string to send + /// a byte array using our protocol to send the message public static byte[] GetMessageToSend(string messageToSend) { dynamic json = new @@ -53,6 +57,12 @@ namespace Util return Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(json)); } + /// + /// creates a message for when the doctor wants to log in. + /// + /// the username of the doctor + /// the (hashed) password of the doctor + /// a byte array using our protocol that contains the username and password of the doctor public static byte[] LoginAsDoctor(string mUsername, string mPassword) { dynamic json = new @@ -68,6 +78,13 @@ namespace Util return Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(json)); } + /// + /// gets the username and password from a given message array. + /// + /// the array of bytes containing the message + /// the username variable that the username will be put into + /// the password variable that the password will be put into + /// true if the username and password were received correctly, false otherwise public static bool GetUsernamePassword(byte[] jsonbytes, out string username, out string password) { dynamic json = JsonConvert.DeserializeObject(Encoding.ASCII.GetString(jsonbytes)); @@ -85,6 +102,12 @@ namespace Util } } + /// + /// gets message using our protocol of the given identifier and data. + /// + /// the identifier string of the message + /// the payload data of the message + /// a byte array containing the json message with the given parameters, using our protocol. private static byte[] getJsonMessage(string mIdentifier, dynamic data) { dynamic json = new @@ -95,6 +118,11 @@ namespace Util return getMessage(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(json)), 0x01); } + /// + /// gets a message using our protocol with only the given identifier string. + /// + /// the identifier to put into the message + /// a byte array containing the json with only the identifier, using our protocol. private static byte[] getJsonMessage(string mIdentifier) { dynamic json = new @@ -104,11 +132,21 @@ namespace Util return getMessage(Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(json)), 0x01); } + /// + /// gets the login response of the given status + /// + /// the status of the response + /// a byte array containing the response for the given status, using our protocol. public static byte[] getLoginResponse(string mStatus) { return getJsonMessage(LOGIN_RESPONSE, new { status = mStatus }); } + /// + /// gets the status of the given json message + /// + /// the byte array containing a json message using our protocol + /// the response of the message public static string getResponseStatus(byte[] json) { return ((dynamic)JsonConvert.DeserializeObject(Encoding.ASCII.GetString(json))).data.status; @@ -195,6 +233,11 @@ namespace Util return getMessage(payload, 0x01); } + /// + /// gets the message to start a session with the given user username + /// + /// the username of the user we want to start the session for + /// a byte array containing the message to start the session of the given user, using our protocol. public static byte[] getStartSessionJson(string user) { dynamic data = new @@ -204,6 +247,11 @@ namespace Util return getJsonMessage(START_SESSION, data); } + /// + /// gets the message to stop a session with the given user username + /// + /// the username of the user we want to stop the session for + /// a byte array containing the message to stop the session of the given user, using our protocol. public static byte[] getStopSessionJson(string user) { dynamic data = new @@ -212,7 +260,13 @@ namespace Util }; return getJsonMessage(STOP_SESSION, data); } - + + /// + /// gets the message to set the resistance of the given user with the given resistance. + /// + /// the username to set the resistance of. + /// the resistance value to set + /// a byte array containing a json messsage to set the user's resistance, using our protocol. public static byte[] getSetResistanceJson(string user,float mResistance) { dynamic data = new @@ -223,6 +277,11 @@ namespace Util return getJsonMessage(SET_RESISTANCE, data); } + /// + /// gets the response message with the given value. + /// + /// the boolean value to indicate if the operation we want to send a response for was successful or not. + /// a byte array containing a json message with the response and the given value. public static byte[] getSetResistanceResponseJson(bool mWorked) { dynamic data = new @@ -232,6 +291,11 @@ namespace Util return getJsonMessage(SET_RESISTANCE, data); } + /// + /// gets the message to indicate a new connection for the given user. + /// + /// the username of the user to start a connection for. + /// a byte array containing a json message to indicate a new connection for the given user, using our protocol. public static byte[] getNewConnectionJson(string user) { if (user == null) @@ -243,6 +307,11 @@ namespace Util return getJsonMessage(NEW_CONNECTION, data); } + /// + /// gets the message for when a user has been disconnected. + /// + /// the username of the user that has been disconnected + /// a byte array containing a json message to indicate that the given user has disconnected, using our protocol. public static byte[] getDisconnectJson(string user) { dynamic data = new @@ -252,31 +321,62 @@ namespace Util return getJsonMessage(DISCONNECT, data); } + /// + /// gets the resistance from the given json message + /// + /// the json messag + /// the resistance that was in the message public static float getResistanceFromJson(byte[] json) { return ((dynamic)JsonConvert.DeserializeObject(Encoding.ASCII.GetString(json))).data.resistance; } + /// + /// gets the resistance response from the given json message + /// + /// the byte array containin the json message + /// the response of the message, so wether it was successful or not. public static bool getResistanceFromResponseJson(byte[] json) { return ((dynamic)JsonConvert.DeserializeObject(Encoding.ASCII.GetString(json))).data.worked; } + /// + /// gets the username from the given response message. + /// + /// the byte array containin the json message + /// the username in the message. public static string getUsernameFromResponseJson(byte[] json) { return ((dynamic)JsonConvert.DeserializeObject(Encoding.ASCII.GetString(json))).data.username; } + /// + /// gets the chat message from the given json message. + /// + /// the byte array containin the json message + /// the chat message in the json message public static string getChatMessageFromJson(byte[] json) { return ((dynamic)JsonConvert.DeserializeObject(Encoding.ASCII.GetString(json))).data.chat; } + /// + /// gets the username from the given json message. + /// + /// the byte array containin the json message + /// the username that is in the message public static string getUsernameFromJson(byte[] json) { return ((dynamic)JsonConvert.DeserializeObject(Encoding.ASCII.GetString(json))).data.username; } + /// + /// gets the byte array with the json message to send a message with the given parameters. + /// + /// the username of the user that wants to send the message + /// the message the user wants to send + /// a byte array containing a json message with the username and corresponding message, using our protocol. public static byte[] getChatJson(string user, string message) { dynamic data = new From 31be096b94afc74e5c543f665b5fe6b7250ca018 Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 11:57:42 +0200 Subject: [PATCH 05/16] added small comments --- ClientApp/Utils/Client.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ClientApp/Utils/Client.cs b/ClientApp/Utils/Client.cs index cef78ab..21bcf83 100644 --- a/ClientApp/Utils/Client.cs +++ b/ClientApp/Utils/Client.cs @@ -150,11 +150,13 @@ namespace ClientApp.Utils Console.WriteLine("Set resistance identifier"); if (this.handler == null) { + // send that the operation was not successful if the handler is null Console.WriteLine("handler is null"); sendMessage(DataParser.getSetResistanceResponseJson(false)); } else { + // set the resistance in the vr scene and send that it was successful float resistance = DataParser.getResistanceFromJson(payloadbytes); this.handler.setResistance(resistance); engineConnection.BikeResistance = resistance; From 4a238d920758a90be251f37583864656ae4303f6 Mon Sep 17 00:00:00 2001 From: shinichi Date: Mon, 19 Oct 2020 12:07:00 +0200 Subject: [PATCH 06/16] Auto stash before checking out "HEAD" --- DoctorApp/Utils/Client.cs | 4 ++-- DoctorApp/Views/MainView.xaml | 2 +- Server/Client.cs | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DoctorApp/Utils/Client.cs b/DoctorApp/Utils/Client.cs index f5fa102..15669b6 100644 --- a/DoctorApp/Utils/Client.cs +++ b/DoctorApp/Utils/Client.cs @@ -111,10 +111,10 @@ namespace DoctorApp.Utils Console.WriteLine("Set resistance identifier"); break; case DataParser.NEW_CONNECTION: - this.MainViewModel.NewConnectedUser(DataParser.getUsernameFromResponseJson(payloadbytes)); + this.MainViewModel.NewConnectedUser(DataParser.getUsernameFromJson(payloadbytes)); break; case DataParser.DISCONNECT: - this.MainViewModel.DisconnectedUser(DataParser.getUsernameFromResponseJson(payloadbytes)); + this.MainViewModel.DisconnectedUser(DataParser.getUsernameFromJson(payloadbytes)); break; default: Console.WriteLine($"Received json with identifier {identifier}:\n{Encoding.ASCII.GetString(payloadbytes)}"); diff --git a/DoctorApp/Views/MainView.xaml b/DoctorApp/Views/MainView.xaml index dc14aee..80cf8c9 100644 --- a/DoctorApp/Views/MainView.xaml +++ b/DoctorApp/Views/MainView.xaml @@ -11,7 +11,7 @@ - + diff --git a/Server/Client.cs b/Server/Client.cs index 74425ef..7a89f5f 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -65,6 +65,8 @@ namespace Server } } + if (ar == null || (!ar.IsCompleted) || (!this.stream.CanRead)) + return; this.stream.BeginRead(this.buffer, 0, this.buffer.Length, new AsyncCallback(OnRead), null); } From 689f7030e4d2a5ef98721f05efb9a40444a6f2df Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 12:20:26 +0200 Subject: [PATCH 07/16] added displaying of last doctor message in vr scene --- ClientApp/Utils/Client.cs | 3 +++ ClientApp/Utils/EngineConnection.cs | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ClientApp/Utils/Client.cs b/ClientApp/Utils/Client.cs index 21bcf83..f02d55c 100644 --- a/ClientApp/Utils/Client.cs +++ b/ClientApp/Utils/Client.cs @@ -163,6 +163,9 @@ namespace ClientApp.Utils sendMessage(DataParser.getSetResistanceResponseJson(true)); } break; + case DataParser.MESSAGE: + engineConnection.DoctorMessage = DataParser.getChatMessageFromJson(payloadbytes); + break; default: Console.WriteLine($"Received json with identifier {identifier}:\n{Encoding.ASCII.GetString(payloadbytes)}"); break; diff --git a/ClientApp/Utils/EngineConnection.cs b/ClientApp/Utils/EngineConnection.cs index c852058..4fe35a3 100644 --- a/ClientApp/Utils/EngineConnection.cs +++ b/ClientApp/Utils/EngineConnection.cs @@ -43,8 +43,8 @@ namespace ClientApp.Utils private static string headId = string.Empty; private static string groundPlaneId = string.Empty; private static string terrainId = string.Empty; - private static string lastMessage = "No message received yet"; + public string DoctorMessage { get; set; }; public float BikeSpeed { get; set; } public float BikePower { get; set; } public float BikeBPM { get; set; } @@ -65,6 +65,7 @@ namespace ClientApp.Utils BikePower = 0; BikeBPM = 0; BikeResistance = 50; + DoctorMessage = "No message received yet"; updateTimer = new System.Timers.Timer(1000); updateTimer.Elapsed += UpdateTimer_Elapsed; updateTimer.AutoReset = true; @@ -324,7 +325,7 @@ namespace ClientApp.Utils { // TODO check if is drawn }); - SendMessageAndOnResponse(mainCommand.showMessage(panelId, "message", lastMessage), "message", + SendMessageAndOnResponse(mainCommand.showMessage(panelId, "message", DoctorMessage), "message", (message) => { // TODO check if is drawn From dcf6c3c6d0da567aa7a0f76a4fbc59db4e9913fe Mon Sep 17 00:00:00 2001 From: shinichi Date: Mon, 19 Oct 2020 12:23:52 +0200 Subject: [PATCH 08/16] client can see if doctor connected --- ClientApp/Utils/Client.cs | 6 ++++++ ClientApp/ViewModels/LoginViewModel.cs | 10 ++++++++++ Server/Communication.cs | 11 ++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ClientApp/Utils/Client.cs b/ClientApp/Utils/Client.cs index 21bcf83..3315a05 100644 --- a/ClientApp/Utils/Client.cs +++ b/ClientApp/Utils/Client.cs @@ -163,6 +163,12 @@ namespace ClientApp.Utils sendMessage(DataParser.getSetResistanceResponseJson(true)); } break; + case DataParser.NEW_CONNECTION: + this.LoginViewModel.DoctorConnected(DataParser.getUsernameFromJson(payloadbytes)); + break; + case DataParser.DISCONNECT: + this.LoginViewModel.DoctorDisconnected(DataParser.getUsernameFromJson(payloadbytes)); + break; default: Console.WriteLine($"Received json with identifier {identifier}:\n{Encoding.ASCII.GetString(payloadbytes)}"); break; diff --git a/ClientApp/ViewModels/LoginViewModel.cs b/ClientApp/ViewModels/LoginViewModel.cs index 7b9bfcf..0ec1488 100644 --- a/ClientApp/ViewModels/LoginViewModel.cs +++ b/ClientApp/ViewModels/LoginViewModel.cs @@ -42,5 +42,15 @@ namespace ClientApp.ViewModels this.MainWindowViewModel.SelectedViewModel = new MainViewModel(MainWindowViewModel); } } + + internal void DoctorConnected(string name) + { + this.MainWindowViewModel.InfoModel.DoctorConnected = true; + } + + internal void DoctorDisconnected(string name) + { + this.MainWindowViewModel.InfoModel.DoctorConnected = false; + } } } diff --git a/Server/Communication.cs b/Server/Communication.cs index 6e173cd..7a093fc 100644 --- a/Server/Communication.cs +++ b/Server/Communication.cs @@ -22,6 +22,7 @@ namespace Server this.clients.ForEach((client) => { this.mDoctor.sendMessage(DataParser.getNewConnectionJson(client.username)); + client.sendMessage(DataParser.getNewConnectionJson(this.mDoctor.username)); }); } } @@ -58,13 +59,21 @@ namespace Server public void NewLogin(Client client) { this.clients.Add(client); - Doctor?.sendMessage(DataParser.getNewConnectionJson(client.username)); + if (this.Doctor != null) + { + Doctor.sendMessage(DataParser.getNewConnectionJson(client.username)); + client.sendMessage(DataParser.getNewConnectionJson(Doctor.username)); + } } public void LogOff(Client client) { if (this.Doctor == client) { + this.clients.ForEach((client) => + { + client.sendMessage(DataParser.getDisconnectJson(this.mDoctor.username)); + }); this.Doctor = null; } this.clients.Remove(client); From 973ff20da419f7cf137e062373df491c496bff14 Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 12:49:04 +0200 Subject: [PATCH 09/16] small typo --- ClientApp/Utils/Client.cs | 1 + ClientApp/Utils/EngineConnection.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ClientApp/Utils/Client.cs b/ClientApp/Utils/Client.cs index f02d55c..a47c1fa 100644 --- a/ClientApp/Utils/Client.cs +++ b/ClientApp/Utils/Client.cs @@ -165,6 +165,7 @@ namespace ClientApp.Utils break; case DataParser.MESSAGE: engineConnection.DoctorMessage = DataParser.getChatMessageFromJson(payloadbytes); + Debug.WriteLine("received message from doctor"); break; default: Console.WriteLine($"Received json with identifier {identifier}:\n{Encoding.ASCII.GetString(payloadbytes)}"); diff --git a/ClientApp/Utils/EngineConnection.cs b/ClientApp/Utils/EngineConnection.cs index 4fe35a3..1b86ec2 100644 --- a/ClientApp/Utils/EngineConnection.cs +++ b/ClientApp/Utils/EngineConnection.cs @@ -44,7 +44,7 @@ namespace ClientApp.Utils private static string groundPlaneId = string.Empty; private static string terrainId = string.Empty; - public string DoctorMessage { get; set; }; + public string DoctorMessage { get; set; } public float BikeSpeed { get; set; } public float BikePower { get; set; } public float BikeBPM { get; set; } From 2ecc90ff2c7cc76e6e20d8518181ff9a54f98281 Mon Sep 17 00:00:00 2001 From: shinichi Date: Mon, 19 Oct 2020 12:52:35 +0200 Subject: [PATCH 10/16] doctor can now disconnect --- ClientApp/Utils/EngineConnection.cs | 4 ++-- DoctorApp/Utils/Client.cs | 3 +++ Server/Client.cs | 4 ++-- Server/Communication.cs | 18 +++++++----------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/ClientApp/Utils/EngineConnection.cs b/ClientApp/Utils/EngineConnection.cs index 4fe35a3..6e3f846 100644 --- a/ClientApp/Utils/EngineConnection.cs +++ b/ClientApp/Utils/EngineConnection.cs @@ -44,7 +44,7 @@ namespace ClientApp.Utils private static string groundPlaneId = string.Empty; private static string terrainId = string.Empty; - public string DoctorMessage { get; set; }; + public string DoctorMessage { get; set; } public float BikeSpeed { get; set; } public float BikePower { get; set; } public float BikeBPM { get; set; } @@ -75,7 +75,7 @@ namespace ClientApp.Utils noVRResponseTimer.Elapsed += noVRResponseTimeout; noVRResponseTimer.AutoReset = false; noVRResponseTimer.Enabled = false; - + } private void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) diff --git a/DoctorApp/Utils/Client.cs b/DoctorApp/Utils/Client.cs index 15669b6..81f245a 100644 --- a/DoctorApp/Utils/Client.cs +++ b/DoctorApp/Utils/Client.cs @@ -130,6 +130,8 @@ namespace DoctorApp.Utils expectedMessageLength = BitConverter.ToInt32(totalBuffer, 0); } + if (ar == null || (!ar.IsCompleted) || (!this.stream.CanRead) || !this.client.Connected) + return; this.stream.BeginRead(this.buffer, 0, this.buffer.Length, new AsyncCallback(OnRead), null); } @@ -192,6 +194,7 @@ namespace DoctorApp.Utils public void Dispose() { Debug.WriteLine("client dispose called"); + sendMessage(DataParser.getDisconnectJson(LoginViewModel.Username)); this.stream.Dispose(); this.client.Dispose(); } diff --git a/Server/Client.cs b/Server/Client.cs index 7a89f5f..0364d38 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -65,7 +65,7 @@ namespace Server } } - if (ar == null || (!ar.IsCompleted) || (!this.stream.CanRead)) + if (ar == null || (!ar.IsCompleted) || (!this.stream.CanRead) || !this.tcpClient.Client.Connected) return; this.stream.BeginRead(this.buffer, 0, this.buffer.Length, new AsyncCallback(OnRead), null); @@ -123,7 +123,7 @@ namespace Server //set resistance on doctor GUI break; case DataParser.DISCONNECT: - communication.Disconnect(this); + communication.LogOff(this); break; default: Console.WriteLine($"Received json with identifier {identifier}:\n{Encoding.ASCII.GetString(payloadbytes)}"); diff --git a/Server/Communication.cs b/Server/Communication.cs index 7a093fc..ec44e4e 100644 --- a/Server/Communication.cs +++ b/Server/Communication.cs @@ -19,11 +19,12 @@ namespace Server set { this.mDoctor = value; - this.clients.ForEach((client) => - { - this.mDoctor.sendMessage(DataParser.getNewConnectionJson(client.username)); - client.sendMessage(DataParser.getNewConnectionJson(this.mDoctor.username)); - }); + if (this.mDoctor != null) + this.clients.ForEach((client) => + { + this.mDoctor.sendMessage(DataParser.getNewConnectionJson(client.username)); + client.sendMessage(DataParser.getNewConnectionJson(this.mDoctor.username)); + }); } } public Communication(TcpListener listener) @@ -50,12 +51,6 @@ namespace Server listener.BeginAcceptTcpClient(new AsyncCallback(OnConnect), null); } - internal void Disconnect(Client client) - { - clients.Remove(client); - Doctor.sendMessage(DataParser.getDisconnectJson(client.username)); - } - public void NewLogin(Client client) { this.clients.Add(client); @@ -76,6 +71,7 @@ namespace Server }); this.Doctor = null; } + Doctor?.sendMessage(DataParser.getDisconnectJson(client.username)); this.clients.Remove(client); } From 134bf235c3550e9841e313d566bf20cc6acb57eb Mon Sep 17 00:00:00 2001 From: shinichi Date: Mon, 19 Oct 2020 13:24:38 +0200 Subject: [PATCH 11/16] multiple clients works --- ClientApp/ClientApp.csproj | 6 +++--- Server/Client.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ClientApp/ClientApp.csproj b/ClientApp/ClientApp.csproj index 637f86c..3fdfbd2 100644 --- a/ClientApp/ClientApp.csproj +++ b/ClientApp/ClientApp.csproj @@ -21,13 +21,13 @@ Always - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest Always diff --git a/Server/Client.cs b/Server/Client.cs index 0364d38..acd509f 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -34,7 +34,7 @@ namespace Server private void OnRead(IAsyncResult ar) { - if (ar == null || (!ar.IsCompleted) || (!this.stream.CanRead)) + if (ar == null || (!ar.IsCompleted) || (!this.stream.CanRead) || !this.tcpClient.Client.Connected) return; int receivedBytes = this.stream.EndRead(ar); From 8249632d00f823cf8fd4f44f4191cdb461dbb935 Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 13:31:29 +0200 Subject: [PATCH 12/16] add set resistance --- Server/Client.cs | 3 ++- Server/Communication.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Server/Client.cs b/Server/Client.cs index 7a89f5f..be819e1 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -119,7 +119,8 @@ namespace Server this.communication.StopSessionUser(DataParser.getUsernameFromJson(payloadbytes)); break; case DataParser.SET_RESISTANCE: - bool worked = DataParser.getResistanceFromResponseJson(payloadbytes); + //bool worked = DataParser.getResistanceFromResponseJson(payloadbytes); + communication.SendMessageToClient(DataParser.getUsernameFromJson(payloadbytes), message); //set resistance on doctor GUI break; case DataParser.DISCONNECT: diff --git a/Server/Communication.cs b/Server/Communication.cs index 7a093fc..304606d 100644 --- a/Server/Communication.cs +++ b/Server/Communication.cs @@ -103,5 +103,16 @@ namespace Server } } + + public void SendMessageToClient(string user, byte[] message) + { + foreach (Client c in clients) + { + if (c.username == user) + { + c.sendMessage(message); + } + } + } } } From fb443a33d780d0864eaf8ea4fd5cbd582249e53c Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 13:32:26 +0200 Subject: [PATCH 13/16] add send message --- ClientApp/Utils/Client.cs | 11 +++++++---- ClientApp/Utils/EngineConnection.cs | 2 +- Hashing/DataParser.cs | 2 ++ Server/Client.cs | 4 ++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/ClientApp/Utils/Client.cs b/ClientApp/Utils/Client.cs index ba81623..fa52f8d 100644 --- a/ClientApp/Utils/Client.cs +++ b/ClientApp/Utils/Client.cs @@ -43,6 +43,7 @@ namespace ClientApp.Utils /// private void initEngine() { + Debug.WriteLine("init engine"); engineConnection = EngineConnection.INSTANCE; engineConnection.OnNoTunnelId = RetryEngineConnection; engineConnection.OnSuccessFullConnection = engineConnected; @@ -88,6 +89,7 @@ namespace ClientApp.Utils /// the result of the async read private void OnRead(IAsyncResult ar) { + Debug.WriteLine("got message in client app"); if (ar == null || (!ar.IsCompleted) || (!this.stream.CanRead)) return; @@ -136,7 +138,7 @@ namespace ClientApp.Utils } break; case DataParser.START_SESSION: - Console.WriteLine("Session started!"); + Debug.WriteLine("Session started!"); this.sessionRunning = true; if (engineConnection.Connected && !engineConnection.FollowingRoute) engineConnection.StartRouteFollow(); Debug.WriteLine("start"); @@ -147,25 +149,26 @@ namespace ClientApp.Utils Debug.WriteLine("stop"); break; case DataParser.SET_RESISTANCE: - Console.WriteLine("Set resistance identifier"); + Debug.WriteLine("Set resistance identifier"); if (this.handler == null) { // send that the operation was not successful if the handler is null - Console.WriteLine("handler is null"); + Debug.WriteLine("handler is null"); sendMessage(DataParser.getSetResistanceResponseJson(false)); } else { // set the resistance in the vr scene and send that it was successful float resistance = DataParser.getResistanceFromJson(payloadbytes); + Debug.WriteLine("resistance set was " + resistance); this.handler.setResistance(resistance); engineConnection.BikeResistance = resistance; sendMessage(DataParser.getSetResistanceResponseJson(true)); } break; case DataParser.MESSAGE: + Debug.WriteLine("client has received message from doctor"); engineConnection.DoctorMessage = DataParser.getChatMessageFromJson(payloadbytes); - Debug.WriteLine("received message from doctor"); break; case DataParser.NEW_CONNECTION: this.LoginViewModel.DoctorConnected(DataParser.getUsernameFromJson(payloadbytes)); diff --git a/ClientApp/Utils/EngineConnection.cs b/ClientApp/Utils/EngineConnection.cs index 1b86ec2..9f5a816 100644 --- a/ClientApp/Utils/EngineConnection.cs +++ b/ClientApp/Utils/EngineConnection.cs @@ -225,7 +225,6 @@ namespace ClientApp.Utils string handLeftId = JSONParser.GetIdSceneInfoChild(message, "LeftHand"); string handRightId = JSONParser.GetIdSceneInfoChild(message, "RightHand"); groundPlaneId = JSONParser.GetIdSceneInfoChild(message, "GroundPlane"); - Write("--- Ground plane id is " + groundPlaneId); }); // add the route and set the route id CreateTerrain(); @@ -338,6 +337,7 @@ namespace ClientApp.Utils private void SetFollowSpeed(float speed) { + Write("starting route follow"); WriteTextMessage(mainCommand.RouteFollow(routeId, bikeId, speed, new float[] { 0, -(float)Math.PI / 2f, 0 }, new float[] { 0, 0, 0 })); WriteTextMessage(mainCommand.RouteFollow(routeId, cameraId, speed)); } diff --git a/Hashing/DataParser.cs b/Hashing/DataParser.cs index a3d7fc4..ff6f5ec 100644 --- a/Hashing/DataParser.cs +++ b/Hashing/DataParser.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; @@ -338,6 +339,7 @@ namespace Util /// the response of the message, so wether it was successful or not. public static bool getResistanceFromResponseJson(byte[] json) { + Debug.WriteLine("got message " + Encoding.ASCII.GetString(json)); return ((dynamic)JsonConvert.DeserializeObject(Encoding.ASCII.GetString(json))).data.worked; } diff --git a/Server/Client.cs b/Server/Client.cs index be819e1..d7c6e6c 100644 --- a/Server/Client.cs +++ b/Server/Client.cs @@ -122,10 +122,14 @@ namespace Server //bool worked = DataParser.getResistanceFromResponseJson(payloadbytes); communication.SendMessageToClient(DataParser.getUsernameFromJson(payloadbytes), message); //set resistance on doctor GUI + break; case DataParser.DISCONNECT: communication.Disconnect(this); break; + case DataParser.MESSAGE: + communication.SendMessageToClient(DataParser.getUsernameFromJson(payloadbytes), message); + break; default: Console.WriteLine($"Received json with identifier {identifier}:\n{Encoding.ASCII.GetString(payloadbytes)}"); break; From 107d95b81a1f1586c4276bd4d40918c3387ac483 Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 13:59:47 +0200 Subject: [PATCH 14/16] fixed overflowexception --- ProftaakRH/BikeSimulator.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ProftaakRH/BikeSimulator.cs b/ProftaakRH/BikeSimulator.cs index d1c3ae7..1f8e7c0 100644 --- a/ProftaakRH/BikeSimulator.cs +++ b/ProftaakRH/BikeSimulator.cs @@ -93,17 +93,23 @@ namespace Hardware.Simulators //Generate an ANT message for page 0x10 private byte[] GenerateBike0x10() { - //SOMEONE FIX THIS!!!!!!!!! + try { - byte[] bikeByte = { 0x10, Convert.ToByte(equipmentType), Convert.ToByte(elapsedTime * 4 % 64), Convert.ToByte(distanceTraveled), speedArray[0], speedArray[1], Convert.ToByte(BPM), 0xFF }; + byte[] bikeByte = { 0x10, check(equipmentType), check(elapsedTime * 4 % 64), check((int)Math.Round(distanceTraveled)), speedArray[0], speedArray[1], check(BPM), 0xFF }; return bikeByte; } catch (OverflowException e) { Debug.WriteLine(e); - return GenerateBike0x10(); } + byte[] res = { 0x10,0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0xFF}; + return res; + } + + private byte check(int value) + { + return value > 255 ? Convert.ToByte(0) : Convert.ToByte(value); } //Generate an ANT message for BPM From f934dee2d0f49148c99fecd0328b6e5069d054b8 Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Mon, 19 Oct 2020 14:01:39 +0200 Subject: [PATCH 15/16] made overflow return 255 instead of 0 --- ProftaakRH/BikeSimulator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ProftaakRH/BikeSimulator.cs b/ProftaakRH/BikeSimulator.cs index 1f8e7c0..e0d0ccd 100644 --- a/ProftaakRH/BikeSimulator.cs +++ b/ProftaakRH/BikeSimulator.cs @@ -102,14 +102,14 @@ namespace Hardware.Simulators catch (OverflowException e) { Debug.WriteLine(e); + byte[] res = { 0x10,0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0xFF}; + return res; } - byte[] res = { 0x10,0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0xFF}; - return res; } private byte check(int value) { - return value > 255 ? Convert.ToByte(0) : Convert.ToByte(value); + return value > 255 ? Convert.ToByte(255) : Convert.ToByte(value); } //Generate an ANT message for BPM From de9716128cc093d0d8e23b97c03f15fb6dc8bd78 Mon Sep 17 00:00:00 2001 From: shinichi Date: Mon, 19 Oct 2020 14:02:03 +0200 Subject: [PATCH 16/16] restyle tabheader --- ClientApp/Utils/EngineConnection.cs | 2 +- DoctorApp/DoctorApp.csproj | 4 ++++ DoctorApp/Views/MainView.xaml | 5 ++++- DoctorApp/img/patient.png | Bin 0 -> 11413 bytes 4 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 DoctorApp/img/patient.png diff --git a/ClientApp/Utils/EngineConnection.cs b/ClientApp/Utils/EngineConnection.cs index 7d56a3b..5113262 100644 --- a/ClientApp/Utils/EngineConnection.cs +++ b/ClientApp/Utils/EngineConnection.cs @@ -27,7 +27,7 @@ namespace ClientApp.Utils //new PC("DESKTOP-M2CIH87", "Fabian"), //new PC("T470S", "Shinichi"), //new PC("DESKTOP-DHS478C", "semme"), - new PC("HP-ZBOOK-SEM", "Sem") + //new PC("HP-ZBOOK-SEM", "Sem") //new PC("DESKTOP-TV73FKO", "Wouter"), //new PC("DESKTOP-SINMKT1", "Ralf van Aert"), //new PC("NA", "Bart") diff --git a/DoctorApp/DoctorApp.csproj b/DoctorApp/DoctorApp.csproj index 2ff8d95..14b43b7 100644 --- a/DoctorApp/DoctorApp.csproj +++ b/DoctorApp/DoctorApp.csproj @@ -9,12 +9,16 @@ + Always + + PreserveNewest + diff --git a/DoctorApp/Views/MainView.xaml b/DoctorApp/Views/MainView.xaml index 80cf8c9..26ab386 100644 --- a/DoctorApp/Views/MainView.xaml +++ b/DoctorApp/Views/MainView.xaml @@ -11,7 +11,10 @@ - + + + + diff --git a/DoctorApp/img/patient.png b/DoctorApp/img/patient.png new file mode 100644 index 0000000000000000000000000000000000000000..03dda309daac63016249d76e5b78a34e8d2abde1 GIT binary patch literal 11413 zcmZX4RahKc&+QBh0}Sp~+}+)Mu;NnODN>}kyF<}pEiT2aP#gx=;!g2W++F8?zuR*z zl6Ae4C&}8$iq&|phyfvm0000CC8(U%e;D#VLjnKi`z0*K{{vK4=m&QI01fwl2Ao|F zdI12a07`PwIzFqXCdfWIx_4O8_iJ#LS`+EY2o`n^4J`&fPZJMMZvttY(uor69GdcF z{f3UCA}tx4C{YL9@pLHf4e#* z?qALh@4F)+qDn374M;QoKX}~ZtZ-(m)4s~>%suj=FPRb0GVE44!-L0LaqMQTz90Se zv0cCaGXlrqBVNh}jtxR|!UP3nIR=3txVlUf!)*6z2;*+fgSDm~P_H!|bJ}s28?vnde3{IDNA*%wv{P z8-2p4H|Vd)N=G;BQc(73Ok$rYhpq*v5$|Xp-$kx)`3zc@CD^3F8u*ZBGp%KxcG#e1 z>Glk((ZdJtL^A7GvlHNUh&(INR4YkSn+bf#`R)X>zP2H_R3q zkJaI~dHGQB-!wR>7oG2%{@#wf$PS2E<`ko3cM9RTQu0jh$yN@zrx;U+Evdj@RwEkL z4&8H8a{Dt9ZWHlx`<2HZDL2#3)TW?{=1PZ}QTu9r?yDa*dK#Lf(UnqCB^yE;H2pi9 z=53N_7#MM{`OEUZc+d)b6sC|&Z4k@!cO<&*Ogk|Dl5$)Ci*(lOyB(jfDa7o-Ln`CJGbm4>Za|6)B3!BhPlv+ zN|)E@{C2cAklO{_bbfn1AIf1m4Qm2S3G=iHW#00|esl_O@G15lZzfm8LW=?{8;Cq0 zVdt9qDf;OL{3wQ^xtS4}@Mk&Yz-_2X*v6En*?m6OHBoHhCW)(e#mlkXa*3U#SQ_xG z>u#!IDLwgL{Hhm?&?)d{WKKEv17>e3KL@V5c+v|&-iGJH27?vGO}Qw^cCEL6wP9*= z;zqsWFF3JrrVdIHoa+zoWu_0;UZE_>i;!~_?HRWE$+TD?g{^@$2`67lq4f;?XlV5t z3>{Wj`JG@kpx3Dwqqlg=DCbi5wQFn937m@Z@U6sQIlPC}B>n^6FRwVWz0aeT>&@dO zl>Vt8(4F7XZi(n9GGjv${P{f{H)Wb13Lw-Hl_HLH+C@G}@(Z{y70ig#Ny*rpzBjkqj`p@i9v;uAVl{TSH}qN5GyaW8y}~&ycdK+$xrb!UhSAyk>#2<9Zv`OH z@-6H9{lP{lCqSEwWJ8(fqe)Wf7y0O3&;&v!qsU7{PS z+9he^&k_ixDdET(p@MSCsQ7;nC}GD@q6W*LXn-`c+;8@m@u9O)QnNF+q+S`?U)??~ zgBaT_gq$`E5G(Ap(%q&M|HYSQRjn4GSy_qj#AS(HxClx`PreptpT`#-nMNR|>#rme6hu=k}=&C>`u^08|4C&=XlA}!%SQvAgV|4qn{Ni8S z+q|K?_Ju&0YimPu`mJwc;^s~%&}Y3?jT0oN4ot|f%=HNJhB`0+EQ@Tfh;!E}89ffB zi8z#%!cDCdVS`CudiSlR{VX!pXQg;COVzf#2xD~~Tcw!)#M)vJ6SP>?pl3$6?z>x0 zk^G!oBKt->>phlVxUUu*L7{I3S?3ts5aSEaUaa-x1cKy0(UWXV-+f?acI8-StnlSQ z47*E7CwATZ2nKf4`A(Xf4{?43(HxdKA?(YJQB1Es4w%0ch-vGdDP74cj#EWX#q1Af z4Qwk6MN!_Iu>a|A6J$;8%JLKAty2Slo_;;TO;i1%#r1*VcZ~chwxiRpV-{QoZEjAC z2Bp$}Fl18{FWkOMf#b`^gt4tgMR1HI`VX|X*K2jCX~V_*R3&X4&D#9COoDNDa{W5+ z?-iz+dAR9rAwSne#i;(iqPmiIV1xj@&8DJC*QqoX6z%8Bob~n7>|8r(BP&9gD9Mkyv9Z{6p8FFagvG}l;HP0?sOg}A~3qEHF;|Cpgt3B( z@SEZS%3M#YR+`qVb9H(CssQVIY*jtc85XT|;pv>-^sXMWK}<{#lpR*I4ib`@M+Y!a z5rb`c!nQcUC{A4Hh^Ns}P3ZXI&Z(`#Di9EaJn^G$JJSk}a>-84{7hi*Y>(4z%Q4X|Mp0X6}sCX$rYb&9EKoK~@%)XEtc%+K$sqd=Yn(_qEs6$tvYJ!_%7|yD9vY{CFvd@o}iC6Noua)=rt$wYpQ>jJVJ^IF3{7dtKP! zXNvr-8zm)o;i9)ta-XU3y^jjKKgx<8sY_GUr{@5^JNaW{_>5LmuX31xY z`tgkcEPQ0_JNoDIdaDS*q|R};tC_+ce%6bwp<~2H;Z7%@&`JY)b^buhn2ewA3B;RY z-%2}$fFgIqBkhZ07nq-(q&L z_vs;H&56W7=8?R&u@64sQ+^dnc^YXbrFZ<#jk@6FnR=BEcPZ1=q* zpBqh*#gD*3$JOtrxht@g5z($M;fw5kTp9;HOhfY8^bTfP$9dC)=X1;zb=02K7M6aB zTDptaZhw>#5^qo`gzihnG)KigM3Qq9s>)W<4G>eHs_dSw-|1c-u@uD9uA{shB)%O+ ziG_68a0QYCC5kiJu)d7?lgna-)q%O^G=E0)3i!5qHR@WZXXy`ZFT8#_7y*1 zwhw5my~A(R!Cv9LnK;ahdcY~6BO!3IR!r@d`(!+%Wof3z72fiWyd9UgZe zmx4wR;D*tZDy;(S%yVnKEeal;B-rRcl@jC@beE7=q&_<^j^xT1FF;Ad>?T+e7=UY1r|0-u8d+~Di zz0!+q3diQ3S5;_01^CK@Ovf~YHR_6CfcXKBxXz1W<0ZLVqha`7Pzm59rs#$#XuEdZ z?!K$VNp~3_|Fk#ZWZ79+3%w%%#MC(%f*VOFhK-^V50}t}%#TXu+A3AKOMm&`omZ%Pv!h$NO!z@}D3$QBi zWRH4e^S(2lHYvFAf)T^ z)5EGp&@l8QF%_urE&d|`M z#<;>=QG~zi6)W!^FbCED5NQBWLVlry{2h;y0Y4pd5haAcev96;V{-?Fzbp}{8B<`XuIUTJxa(cpM_;*md=bz^s+wHayLfeFL63A z!G6R8zua)u9~*TvA(8H90!o0~M`SaOuEpw{tSm5jy1mHnX z`coTl6eO_YK>$mTJ`S3vtPACxI{3&HsS{NUHy3&_3&?JLCGdyuwZ_q>y|4o)I9u zNPY>}1U^x1kG@fSUfs8;1vb)zppSfm&Hkpa(jX4Qv?wtnB9cx1S67UhK!2w!a_qD@4yc{PAk?fXyr6#wj)h_rzrG+`{|^Oe5dLeJIj-^COe$npE* z9B`-VB>#_ z3d@qXC(p&RTVMHZS8?{Ct%eqTH-o3K-e7Y->s30TxaWs1uUO+jK^OMuLKNq3pMa=< zC1ZX$$uffw2~8a5IzlC6D`!(909Q2?S$Cj6l3zE8 zsqRGHaJo2VZjZZ_Tp@$vT2wnkrlq9GQpW?scI5T=Go^b0W4U#&0wG}7u&bo-EYpS= za7!iKfo@&PX#&#$r+2|mj_cp$8*_Z?$d+#HGtpedS{VXV_3b*5o=qUItYDIKW_mW} z=ba25p5`Z=Nq_XRbw0w+F0CoA9^GE*9}&4?vZ4P~is+{kga0apHx;cyg`f_M3k(DfUjj!Fx(qYHa|Fw$z)bUNDj?Nid|T7%IBmrnj>o>-kJzejI~VI^pE~@RKs?E z#i0rI18GFKjSH)(`iaiBNlz@DD_K~!XZZ1Mj1&F)u=uGeVtX^h6t-HqUx4Kg{PhnM z5T;n$Rq=TXnt%%f0MssSBC91XR|r6#p#W8mwzMw5LdXWw1lkI%&A@y4OBrtV5{QPy z7Ws~0d$gb&lkm~?Aq51+1Z>ow2n%{xdv_NV*(QUsWHvo7R+%8lKt{zins#m8{=^lY z$i%=rO}Sp$W|P!N4ZQ>)ML`oOzoak}1@ae~9MdR&`#80K>ODTtv3uuEMzOt_?(BV+ z9wsd&HS7z^vS!H;3K0M$bXR*q0k949g4MDO9_L5&kvbn-(;#1pck68;EVQcVYa|y` zSqJZ)dKd=K=T8&)oBi%m$g8H$^O4Wu7`F&tFkY{R)_A@_ne;UPFi(^Ur4tq^DqzmE z;O7$1#hNL&ObH5CA86RXj{VI4ptVb z78R`SyU;>ebF;$$Z64-#4-viklUngv>OsMQ+>#LxA$l(Y?1xTN($HGY1fC1OZ#HZ8 ze~~GSp8+ZWGZd9Ie6%g}bp@s@A+W;BofEOUG_J9~pZ0RIFYCO=%JV82*H!gDpHD`u zN8bB=+WJY@TIP1vh+1s{KbQWnvcFCE&=|LPIOvGN zrk7Ac9t9I49H*QxcUL_kH20fAh(4F~)sC^(2cG)*RCyq3Wk(8JG?;wVtv6pqCk}L0 zX)X0cCieAG@wrLLBqNZY5sS&!fSYLxWxJCe-Y&my>_-utJ`JEe{F}v?R3ET4awnzH zL^oqp_c!|_L}LP5I8?3vb>+E%s3oL(b`2R906cqf*NBxi4dLrN3 zttP`lK|K&LM2>j{cwg5gNdrg-^j{3!Kh<^hhljep3fB;WL=vS**J)4{l!{KdGu&eo z@;AYaqTbQ{oCG-zkp8&ffR+xuP1d>Y;az{wdlHCOtsZv>@h5Yv#_!S2fq$o%Q6k z#|2DquVxop#1lfD&*&)`SwD9?x`igYz+)wRK_&9)|-eD5uLJqY~&I8o4uotP^n4n`CvJb3By_XpV{2s03YYS z`D&;o8dIn-!86agtu5WsRE%HoSEO2$kww*&-G_zDLIi*%OehU*Tpv#%3J7foID1B9 zjYCPq23f*{wGz7st%mIZr$8DVli(%zrTPnjs#sCB0T3#oa zp64<0S33sjN8wWo?VpnxD=#z*t7ZS~;N|x>G!!iJ5tK$=pphjk3o5s0){j`}8SqOD zV5m~b!ER3NUswqa_ZN)m>e0DbYt8bPr#m+wkR*c6)~=@g+bzd!fLS^lIWNv_tDO-} zK+rk*^5t;V)}AwB0Sj~XVQKB__Nin-h`OD6bg>T7%P50eQttN8FA~Mz27G4|VbQao zV6}7Fsol}y*})gN=Q(S9E{-&4D!wbRcAp5V&0)j6PXz!8p%FQ6ohy2CTxmG@q4-+jk>bp5ZEsQ|@k>?+jM1vkFkJ4Hj}9%zL^$^yx_mW9zOH$=Jyo z8LCf?`nP+65Qo|>=!T(asu!Su^34{>ttymy(m^y$h93h@`wLaw4k|cwJ1+95fdvoh zxHmpt`Cq8?hP~q4sZ|-cU@tsb`ftQ{FPGfW{)j!yc<+q)`EZ^%TS-#QI&q#RDaYZW z5W0RFT}?@`4ZNiLZ^69ipVe=6FOLf8wGSn*sw_8J)o82ABKmK?T7JxWN96ilI+;OQ zDnWzU$|dXO(+EYEdkxM8hCWx(`8{cNAIA6EGcv}1bK)_*h?=Q4d4gb)T#VXV_pKGd z_`;iA{jD0#`##0ZH!_LmGnI#jHO{TvI*EuYb<=2~jSom|d+M zjKvl^2gY?+3)W09y9kt4427jojdkAM~wK)xs#*LzLC5N54 zy!oas?ORa{fk29Rb2`Q`YnDE(Ig}TzL8-a|MF}{nhHl93d^)lQe+ot@m#ay~a$L3T z_vnModYa$y#M|Z21U7I>rjbY%9q1V}v_QW2e90z0zV$_V4BYyeEb{OE`B3L_`23z` zZ`YP=hrn5|?^J<1TuKkQ>M@idwhfqgWDbu7hN&(N>vN3mA>itv`Y$Th(1VI0OvG#~ zeTw&$euwg{mX~yd&m=h(dip8qj_r2eqo{3U03;UbyFrM z0D-NnnxTOEr8W_$8ctd|%a0f(acPO;T81k2#OC-9oo^(XjK7x!WT3MQ{X0y%@ zp!?dkqw?`Q1j3}HN|pwv+G`wyj%0TC4hVfpVC zXeEntbef*0CUW9Le-QLJCLUcXM%9vscIBOwo{?2GC?2a| zt6C4t6v_F0BB6w!K~Ox5`qKY!D2l8=!m{Q8hH$IDR5*3(c=8Lr>D;qieDxAF6VZr9 z5MPh;`#OSaCrUoV82$I>GIDE8Nnd9EN3dm=X2rF3|Ng_dRphF9{b)2`O4)Qudu9I@ zi;>*}d)iPb+%reyqYfkX$T_gee&@a7EJK{wW%rX>aRP_D_tDQWgk>{b6f<4+&^iIm z&^k9YtGC!Y6)ku$`&6O(A!&QY88;IqkfQN!MmIdTQ`hppUs5dffS+8fIg%2Jy*W~E znlfbm14b5OyvHP}D#3Bnr^@Bk2=EGc-1u+~5E~H{~UGH_nJ;BFnKhT!#$KQ9uJ{oAheE7d9hB0Nk+oF^GTL z2;CSARp1WR`vntI6Z?hc66|x~-hY%t(~byuIAmO2WW^BRKL&j>iX{ab;-UQVI*Ml5 z4grPKF>HjYZ#-|dI^f1bq{jH-Ff?8yIlD0_^05>mtzULjojnFmdL4hnHPgIoYKk$J z-)pLF5qy$TQnLcG0Oa=UbfPjWkq9MN=S7(`raNXu)224PHw?0jFH4k+72R(U01qUl zXR&~ftK0wpi>0)Z)X~;*XgEJG1QVeYqHz36DwiRV=O5;zce9*GwFD`FitET&z>(Vg zGKpguh}7pcoyromo<`r8_FQ^K>3@UG#rifMX}*gS{OfGnE7x(fALm>WLrKwD%7v2}@+Tlg$jV zF}{;KYmVQs>K7k0K^l8cxBje3)?$)>M5$e)tMefF0WDM5@WL!g`;|pjabm!4YykOl z*!jUsxz+#zfS!eE&BIVNTtT}Tg!87piN3+k$XI?GOtzG z=`GC+^qhg;-#s{PVYUr29I`AdBJdjEQByp(_-ypYfx&LA+SX|NJKASv-ZDBHU`h14AfWB#LGfb@xTgSFXw1gg7_=$ z>-t6wD~Q~rnd-qslis1#TY#EZ;)eY}j=ig!65*exqd)Z%_x=&e4B(9XMO=ZrbwZZO zc5If(^oX^uzvYqKZQ{3XhkmYtC*@atyl~TB&k!DN8tevm`8@xlP_98y9)fdREbuLC!a|7G)VjpCvG$ewoPFl7N! zZQ;B-7dWF-;TP5=rOxsqp;c;`x0lyVTxlaa>tHj_|K(B8xZ@ znw;Oobv6Q6m+Lzdq=@bZtB2Ed%c$z7rqpat8h{H3Ab{`Lg$-Ca-LgbNK5)SWCz~%5 zMJT{H$>tJ%2T1S%oCQ-kZ{p6|;j6oMw3~;Mp<-JA zY_IoWf}wl5bc*hCs&Nu7L1o#MTkx{nXk*Ah9s6~7Lhsi!S%niF9DAibUg>LerhjwQ z6eS#eVIF&bNqZ=j09wGL69ZCHBq$(2=s2icsEgmpekqLjOx=x*!{g0g<%!kA;-qJs4cv|pu1ok?c^W@7O?e>dOMqr)$CDV^Gy60gM zQQsID_j(vfa4;`9;g_{C(iMr6r`zv3s1ZCQMzLob+ci*fP?$F(d7y43oA&Ced`@Jx zx96#pXk46PxvOdK5ja0G5GRSM>may$^?^ZwOAOd@=p{mceFQ$hG`y*YVf!!FAWd}g zWUMHF!z8^XoJw;Q5w6IqZ|mAk#ntqEL598hs9bL8ShZ-xT)LiOUxTAE_b*ZlO#9$? z?vHE}dFmt2yBftea9tkh%mc3q5C@Iv`QA$#2gMFpqbqrU+c?6IT3PrsZLz#sA*o{s zkb=(ziA=(@sLcu9-$k->Ywz^P=wYl~x!GfYhGL4eQ9(TRI=^wH4s?T!vnUlH5z5#f z;-U7=JNWF;*wzJv>Xq8(`{v0BNPoJ-r>s#FSP-@8&8tbf;nH#lvOvK7++~3yZDw4B zvDEwAG--ZJ0;4~&sURjGi+ll;IEtryXL#spct{Ce~#N3ne5T?0T}9jyVLkmn5%~mgsdEMGT0ZXDhST_{0*s zClF561zSNO^R7q-N#NH=tiaq5Lz{oZy_3$^c5^lKZPvi@tNS4Yy7i(>mg9!&Y8NpS zh2csO7BDeiy!bmNCYY-o1DVL6wbc6v9smsi?EjuXiXepr5I5NmOKz8g)Cd4w_6y^o zL!0gP^9B?v2EW2W64c(z{N640VSM%F%QcD)(J6<|X+mBKZ@kj*%k9OtCN=Sv#pxP) z4qv`>)(RE#pV!Ho6sYsQ<*aQyhf9e5kPwU9==E6UjQUQn|78d)BGB|A;8eoA5clYo z0>;VCiwlutbR4y19{}oMV(f5YMq8?~pqnQ_b_JddXJ3lkw zBY;vu4YgXorI8Ac$cCvulaM~n&|ah|)}NWxrD9(A21cbhtIaL8>QO0wXkD!UzpI^5 z1P|K$b^Vl~LMWQ}S2I65Sb4-CW!z%-hCaKecPRMt$kQQLQ$k6vPAa(Drz(=Fg9-sa zvp^bAC5M?dadqv2eScjjeL$iofOr=G=PZSEQV)36^Pk^>wI3P~->@SK&S_sI%0j6x z4d2nXp~{ZSGvJb~yUI?ZjKjeGnWey0Fw@X~jdK?vVi}N3;Jchj0+{eY1Xca;I}kMa z^@tgVc}8ONc!6RK(;J03s9*$}3`c!XL?WM4?ln3n+9ZVc=q(J=r{UFG%zG98M}AN_ z3%S0P^pg9WAXgBgQxSJDgQ@--jLnW8>5~F3wXsy zvQ}bfo(MuOr6qQZ7AJnFgTun}aLxUmM+gl> zDGt-62&73tqCgEC@G;=v>E~i&RchPjli^;8~XqW7VgV#Ct;;&pd3^TSbd@ zvkpCW_%t{&&tO7OCEVDAl&+u~9biFE7VBJ6JRcvG{{9E}enerR1b8pdQ-cdrt;{@L zvxfN{klQ3lMx~#_HVcBVy6twitA6~%RCvuV$5=jz*0Hf@R7^dZ2~yD*uF5UubfLwF zKmG%_EhPk#{Q_1X;bDuITS-LSEYv5q^w-CcR2pW%wzy;0Q5=dmaHTpHT?j7Y4sSSa z-rqRie1RtIihA=AC-oqmPbKtJI(YTvh38jpxIl(??)ISg+)r&8Px&V(mEt`awn#JJ zPyrDO{~#!01JhbE_)x{Tgb;T|g3Ax$hiSLf-QY8+aAeUhNKGs-iLF3uxH21l!J=EJ zg1TVkcmU3^#8qxmiPFFRd69kN{UG?u5A@S^BR><`eEnwvPC#U&2s3t zOEYnK;jIVTUTjvTV%Q!4OTs0V)WqSQ6@|v9Fi?9gu4yJ+y;^2bVxo`?ra@`wFc)>g zsR{JdvcvXusmLkXxAlm-eF+Pq3w4lE^uuH-FzONnAFu(&U2O5XFFb6g+WVs@HqD*L z8}6hU({cHlUaZ`&{y}$@%-V-0H{QkGrQQl$AmSb|#slx8iIla(8XOVSFWSkIvJUbJ zz3AqG9rA)`zSm7f^0+{MI7}zoY6HL|?Iv!GJAxw+6T=D9uwMFIG`z^(h z3>*x-d^yT>%WYtLvi`leg&qnZ@lBoG#Hpoqo%T)qVifXYqXgluOoi1&9m+aJy#Vt< zAW4uF*dTOLum1kxFZKhA#~Odu=}@JzQpFZYj zyW3WL`5|ydL3sV?d*L@*I&_AzxsK|biBv~}1@ed5w4)bMPpjV$NMcylYn8R1L3o1! zUwt?oDBMDs_A_mbLtlFd=cuZj0vw#}3qdG|eIYf(14-e5mYF@!>175?88JjgZ-r$m1VyV#}YOF2GRfpRMO( h`+s1(&f+mxz4ae|thUYae>!7;lKgwQS{d`O{{!bQju-#{ literal 0 HcmV?d00001