using Comal.Classes; using InABox.Core; using InABox.DigitalMatter; using NPOI.HSSF.Record.CF; using PRS.Shared; using Syncfusion.Data; using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Media.Media3D; namespace PRSServer { /// /// Represents one conversation between OEM and our GPS Engine. /// internal class OEMConnection { private TcpClient Client; private NetworkStream Stream; private byte[] Buffer; private List ConsolidatedBuffer; private bool Finished = false; private bool Confirmed = false; private Task? ReadTask; private CancellationTokenSource TokenSource = new(); // References to necessary caches private GPSDeviceCache Devices; private GPSUpdateQueue Queue; // Fields of this conversation; these should be initialised by the 'Hello' message. private string Product; private string Serial; // Anything more recent than this threshold is ignored. private static TimeSpan AgeThreshold = TimeSpan.FromMinutes(2); // Data that forms the the request. private List Data = new(); public delegate void CompletedEvent(); public CompletedEvent? OnCompletion; public OEMConnection(TcpClient client, GPSDeviceCache cache, GPSUpdateQueue queue) { Client = client; Stream = Client.GetStream(); Devices = cache; Queue = queue; } private DMHelloResponse HandleHello(DMHelloRequest hello) { Product = DMFactory.GetDeviceName(hello.ProductID); Serial = hello.SerialNumber.ToString(); Logger.Send(LogType.Information, Serial, string.Format("Hello {0} ({1})", Product, Serial)); return new DMHelloResponse(); } private DMMessage? HandleData(DMDataRequest data) { Logger.Send(LogType.Information, Serial, string.Format("{0} DataRecords Received", data.Records.Length)); var iRecord = 1; foreach (var record in data.Records) { Logger.Send(LogType.Information, Serial, string.Format("- Data Record #{0}: {1:dd MMM yy hh-mm-ss} ({2} Fields)", iRecord, record.TimeStampToDateTime(record.TimeStamp), record.Fields.Length)); iRecord++; foreach (var field in record.Fields) Logger.Send(LogType.Information, Serial, string.Format(" [{0}] {1}: {2}", field.IsValid() ? "X" : " ", DMFactory.GetFieldName(field.Type), field)); } Data.Add(data); return null; } private GPSTrackerLocation? HandleTracker(DMGPSField gps, DMRecord record) { var flags = gps.StatusFlags(); // Sometimes we get a ping that has both "Valid" and "NoSignal" set // In this case, lets treat it as valid if (flags.Contains(GPSStatus.NoSignal) && !flags.Contains(GPSStatus.Valid)) { Logger.Send(LogType.Information, Serial, $"- Skipping: Invalid Signal ({Serial})"); return null; } var timestamp = record.TimeStampToDateTime(record.TimeStamp); var age = timestamp - Devices[Serial].TimeStamp; if (age <= AgeThreshold) { Logger.Send(LogType.Information, Serial, $"- Skipping: Recent Update ({Serial}) {age:mm\\:ss}"); return null; } var device = Devices[Serial]; var location = new GPSTrackerLocation(); location.DeviceID = Serial; location.Tracker.ID = device.ID; location.Location.Timestamp = timestamp; location.Location.Latitude = (double)gps.Latitude / 10000000.0F; location.Location.Longitude = (double)gps.Longitude / 10000000.0F; var analoguedata = record.GetFields().FirstOrDefault(); if (analoguedata != null) location.BatteryLevel = analoguedata.BatteryStrength ?? device.CalculateBatteryLevel(analoguedata.InternalVoltage); return location; } private GPSTrackerLocation? HandleBluetoothTag(DMGPSField gps, DMRecord record, DMBluetoothTag tag) { var tagID = tag.ID(); if (!Devices.ContainsKey(tagID) && tagID.Length == 17 && tagID.Split(':').Length == 6) { var truncated = tagID[..15]; var newtag = Devices.Keys.FirstOrDefault(x => x.StartsWith(truncated)); Logger.Send(LogType.Information, Serial, $"- Truncating BT Tag: {tagID} -> {truncated} -> {newtag}"); if (!string.IsNullOrWhiteSpace(newtag)) tagID = newtag; } if (!Devices.ContainsKey(tagID)) { Logger.Send(LogType.Information, Serial, $"- Skipping: Unknown Tag ({tagID})"); return null; } var timestamp = record.TimeStampToDateTime(record.TimeStamp); var age = timestamp - Devices[tagID].TimeStamp; if (age <= AgeThreshold) { Logger.Send(LogType.Information, Serial, $"- Skipping: Recent Update ({tagID}) {age:mm\\:ss}"); return null; } var device = Devices[tagID]; var btloc = new GPSTrackerLocation(); btloc.DeviceID = tagID; btloc.Tracker.ID = device.ID; btloc.Location.Timestamp = timestamp; btloc.Location.Latitude = (double)gps.Latitude / 10000000.0F; btloc.Location.Longitude = (double)gps.Longitude / 10000000.0F; if (tag is DMGuppyBluetoothTag guppy) { btloc.BatteryLevel = device.CalculateBatteryLevel(guppy.BatteryVoltage); //guppy.BatteryVoltage * 5F / 3F; } else if (tag is DMSensorNodeBluetoothTag sensornode) { // Need to check with Kenrick about the calcs here.. // Guppies have 1 battery (ie 1.5V) while Sensornodes have 3 (4.5V) btloc.BatteryLevel = device.CalculateBatteryLevel(sensornode.BatteryVoltage); //btloc.BatteryLevel = sensornode.BatteryVoltage * 5F / 3F; } return btloc; } private IEnumerable HandleRecord(DMRecord record) { if (Devices.ContainsKey(Serial)) { if (record.Fields.FirstOrDefault(x => x is DMGPSField && x.IsValid()) is DMGPSField gps) { if (record.TimeStamp != 0 && gps.Latitude != 0 && gps.Longitude != 0) { if (HandleTracker(gps, record) is GPSTrackerLocation trackerLocation) { yield return trackerLocation; } foreach (DMBluetoothTagList taglist in record.GetFields()) foreach (var item in taglist.Items.Where(x => x.LogReason != 2)) { if (HandleBluetoothTag(gps, record, item.Tag) is GPSTrackerLocation location) { yield return location; } } foreach (DMBluetoothTagData tag in record.GetFields()) if (tag.LogReason != 2 && tag.TimeStamp != 0 && tag.Latitude != 0 && tag.Longitude != 0) { if (HandleBluetoothTag(gps, record, tag.Tag) is GPSTrackerLocation location) { yield return location; } } } else { Logger.Send(LogType.Information, Serial, string.Format("- Skipping: Invalid GPS Data ({0}) {1}{2}{3}", Serial, gps.TimeStamp == 0 ? "Bad TimeStamp " : "", gps.Latitude == 0 ? "Bad Latitude " : "", gps.Longitude == 0 ? "Bad Longitude " : "").Trim()); } } else { Logger.Send(LogType.Information, Serial, string.Format("- Skipping: Missing GPS Data ({0})", Serial)); } } else { Logger.Send(LogType.Information, Serial, string.Format("- Skipping: Unknown Device ({0})", Serial)); } } private DMConfirmResponse HandleConfirm(DMConfirmRequest confirm) { Logger.Send(LogType.Information, Serial, string.Format("Goodbye {0} ({1})", Product, Serial)); var updates = new List(); foreach (var data in Data) foreach (var record in data.Records) { foreach(var update in HandleRecord(record)) { updates.Add(update); } } if (updates.Any()) { Logger.Send(LogType.Information, Serial, string.Format("Sending updates ({0}): {1}", updates.Count, string.Join(", ", updates.Select(x => x.DeviceID).Distinct()))); foreach (var update in updates) { Logger.Send(LogType.Information, Serial, string.Format("- Updating Device Cache: ({0}): {1:yyyy-MM-dd hh:mm:ss}", update.DeviceID, update.Location.Timestamp)); //if (m_devices.ContainsKey(update.DeviceID)) var oldDevice = Devices[update.DeviceID]; Devices[update.DeviceID] = new Device(oldDevice.ID, update.Location.Timestamp, oldDevice.BatteryFormula); } foreach (var update in updates) { Queue.QueueUpdate($"Updated by {Product} ({Serial})", update); } } return new DMConfirmResponse { Status = 1 }; } private void HandleMessage(DMMessage message) { DMMessage? response; if (message is DMHelloRequest hello) { response = HandleHello(hello); } else if (message is DMDataRequest data) { response = HandleData(data); } else if (message is DMConfirmRequest confirm) { response = HandleConfirm(confirm); Confirmed = true; } else { Logger.Send(LogType.Information, "", $"Unknown message type {message.Type}"); response = null; } if(response is not null) { Stream.Write(response.EncodeArray()); } } /// /// /// /// /// Read occurs asynchronously, so essentially this method spins off a read thread and then returns. /// If we call before data comes in, the read does not occur. /// private void DoRead() { var readData = false; // We pass in the cancellation token to prevent the task from continuing if we call stop. ReadTask = Stream.ReadAsync(Buffer, 0, Buffer.Length).ContinueWith(t => { if(t.Exception is not null) { if (Finished) { Finish(); } else { Logger.Send(LogType.Error, Environment.CurrentManagedThreadId.ToString(), CoreUtils.FormatException(t.Exception)); } } else { readData = true; var nBytes = t.Result; if (nBytes > 0) { ConsolidatedBuffer.AddRange(Buffer.Take(nBytes)); try { while (ConsolidatedBuffer.Count > 0) { DMMessage? message = null; try { var payloadLength = DMFactory.PeekPayloadLength(ConsolidatedBuffer); if (nBytes == Buffer.Length && ConsolidatedBuffer.Count < payloadLength) { // Probably we need more data. break; } else { message = DMFactory.ParseMessage(ConsolidatedBuffer); } } catch (Exception e) { Logger.Send( LogType.Error, Environment.CurrentManagedThreadId.ToString(), string.Format("Unable to Parse Record: {0} ({1})", e.Message, BitConverter.ToString(ConsolidatedBuffer.ToArray())) ); break; } if (message is not null) { HandleMessage(message); ConsolidatedBuffer.RemoveRange(0, message.CheckSum + 5); } } } catch (Exception e) { Logger.Send(LogType.Error, Environment.CurrentManagedThreadId.ToString(), e.Message + "\n" + e.StackTrace); } } else { Finished = true; } if (Finished) { Finish(); } else { // If we still have stuff to do, try reading again. Note that this isn't a recursive problem or anything, because // this method returns very soon. DoRead(); } } }, TokenSource.Token); // After 15 seconds, close the connection (but only if it has been confirmed). var delay = Task.Delay(15_000).ContinueWith(t => { if (!readData) { Finished = true; if (Confirmed) { Logger.Send(LogType.Error, "", "Closing connection after 15 seconds of silence."); Stream.Close(); } else { Logger.Send(LogType.Error, "", "No data read for 15 seconds, but connection has not been confirmed."); } } }); } public void Run() { Buffer = new byte[2048]; ConsolidatedBuffer = new List(); DoRead(); } private void Finish() { Stream.Close(); Client.Close(); OnCompletion?.Invoke(); } public void Stop() { // Cancel any read tasks which are yet to start. TokenSource.Cancel(); Stream.Close(); Client.Close(); try { // Let the latest task finish. if(ReadTask?.Wait(1000) == false) { Logger.Send(LogType.Error, "", "The thread didn't die in time."); } } catch (AggregateException ae) { foreach(var exception in ae.InnerExceptions) { if(exception is not TaskCanceledException) { throw; } } } } } }