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;
}
}
}
}
}
}