Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/kenric' into nick

# Conflicts:
#	prs.desktop/Panels/Products/Job Requisitions/JobRequisitionsPanel.xaml.cs
Nick-PRSDigital@bitbucket.org 2 anni fa
parent
commit
a27d5c0e29

+ 22 - 0
prs.classes/JobProductMapping.cs

@@ -0,0 +1,22 @@
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Comal.Classes
+{
+    public class JobProductMapping : Entity, IRemotable, IPersistent, IOneToMany<Job>
+    {
+        [CodeEditor(Editable = Editable.Enabled)]
+        public string Code { get; set; }
+        public JobLink Job { get; set; }
+        public ProductLink Product { get; set; }
+
+        public JobProductMapping()
+        {
+            Code = "";
+            Job = new JobLink();
+            Product = new ProductLink(() => this);
+        }
+    }
+}

+ 1 - 1
prs.desktop/MainWindow.xaml

@@ -540,7 +540,7 @@
                     <fluent:Button x:Name="StockSummaryButton" Header="Stock Forecast"
                                    LargeIcon="pack://application:,,,/Resources/kpi.png"
                                    Click="StockSummaryButton_Clicked" MinWidth="60" />
-                    <fluent:Button x:Name="JobRequisitionDashboardButton" Header="Stock Requisitions"
+                    <fluent:Button x:Name="JobRequisitionDashboardButton" Header="Reservation Management"
                                    LargeIcon="pack://application:,,,/Resources/requisition.png"
                                    Click="JobRequisitionDashboardButton_Clicked" MinWidth="60" />
                 </fluent:RibbonGroupBox>

+ 10 - 3
prs.desktop/MainWindow.xaml.cs

@@ -166,9 +166,16 @@ namespace PRSDesktop
                     //var url = App.DatabaseSettings.URLs.FirstOrDefault() ?? "localhost:8000";
                     //ClientFactory.SetClientType(typeof(RPCClient<>), Platform.Wpf, CoreUtils.GetVersion(), () => new RPCClientSocketTransport(url));
                     
-                    var url = RestClient<User>.Ping(App.DatabaseSettings.URLs, out DatabaseInfo info);
-                    ClientFactory.SetClientType(typeof(RestClient<>), Platform.Wpf, CoreUtils.GetVersion(), 
-                        url, true);
+                    var url = RestClient<User>.Ping(App.DatabaseSettings.URLs);
+                    if(url is null)
+                    {
+                        MessageBox.Show("Server is unavailable!");
+                        return;
+                    }
+                    else
+                    {
+                        ClientFactory.SetClientType(typeof(RestClient<>), Platform.Wpf, CoreUtils.GetVersion(), url, true);
+                    }
                     break;
                 
                 case DatabaseType.Local:

+ 2 - 1
prs.desktop/Panels/Jobs/JobGrid.cs

@@ -51,7 +51,8 @@ namespace PRSDesktop
                 new DynamicManyToManyGrid<JobDocument,Job>(),
                 new DynamicManyToManyGrid<JobStyle,Job>(),
                 new DynamicOneToManyGrid<Job,JobDocumentSetTag>(),
-                new JobFormDefinitionGrid()
+                new JobFormDefinitionGrid(),
+                new DynamicOneToManyGrid<Job, JobProductMapping>(),
             });
             return pages;
         }

+ 1 - 0
prs.desktop/Panels/Products/Job Requisitions/JobRequisitionHoldingsReview.xaml.cs

@@ -315,6 +315,7 @@ namespace PRSDesktop
             Movements = movements;
             DefaultStyle = style;
             JobName = jobname;
+            
         }
 
         public void GetStock(Guid jobid, Guid styleid)

+ 1 - 1
prs.desktop/Panels/Products/Job Requisitions/JobRequisitionsPanel.xaml.cs

@@ -176,7 +176,7 @@ namespace PRSDesktop
 
         private void RefreshJobRequiGrids()
         {
-            JobRequiItems.bRefreshing = false;
+            JobRequiItems.bRefreshing = false;           
             JobRequiItems.Refresh(false, true);
         }
 

+ 1 - 1
prs.desktop/Utils/SelectDatabase.xaml.cs

@@ -135,7 +135,7 @@ namespace PRSDesktop
                         if (db.DatabaseType == DatabaseType.Local)
                             info = new IPCClient<User>(DatabaseServerProperties.GetPipeName(db.LocalServerName)).Info();
                         else if (db.DatabaseType == DatabaseType.Networked)
-                            RestClient<User>.Ping(db.URLs, out info);
+                            RestClient<User>.Info(db.URLs, out info);
                         UpdateInfo(key, db, info, border, image, dbver);
                     });
 

+ 204 - 289
prs.server/Engines/GPS/GPSEngine.cs

@@ -1,18 +1,20 @@
-using System;
-using System.Collections;
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Core;
+using InABox.DigitalMatter;
+using InABox.IPC;
+using netDxf.Tables;
+using PRSServer.Engines;
+using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Sockets;
-using System.Threading;
+using System.Text;
 using System.Threading.Tasks;
-using Comal.Classes;
-using InABox.Clients;
-using InABox.Core;
-using InABox.DigitalMatter;
-using InABox.IPC;
-using PRSServer.Engines;
+using System.Timers;
 
 namespace PRSServer
 {
@@ -22,197 +24,237 @@ namespace PRSServer
         public DateTime TimeStamp { get; set; }
         public CoreExpression<GPSBatteryFormulaModel, double>? BatteryFormula { get; set; }
 
-        public Device(Guid id, DateTime timeStamp, CoreExpression<GPSBatteryFormulaModel, double>? batteryFormula)
+        public Device(Guid iD, DateTime timeStamp, CoreExpression<GPSBatteryFormulaModel, double>? batteryFormula)
         {
-            ID = id;
+            ID = iD;
             TimeStamp = timeStamp;
             BatteryFormula = batteryFormula;
         }
 
-        /// <summary>
-        /// Should return a percentage
-        /// </summary>
-        /// <returns></returns>
         public double CalculateBatteryLevel(double batteryValue)
         {
-            if (BatteryFormula != null)
+            if(BatteryFormula != null)
+            {
                 return BatteryFormula.Evaluate(new Dictionary<string, object?>
                 {
                     { nameof(GPSBatteryFormulaModel.BatteryLevel), batteryValue }
                 });
+            }
             return batteryValue;
         }
     }
 
-    /// <summary>
-    ///     TCPServer is the Server class. When "StartServer" method is called
-    ///     this Server object tries to connect to a IP Address specified on a port
-    ///     configured. Then the server start listening for client socket requests.
-    ///     As soon as a requestcomes in from any client then a Client Socket
-    ///     Listening thread will be started. That thread is responsible for client
-    ///     communication.
-    /// </summary>
-    internal class GPSEngine : Engine<GPSServerProperties>
+    public class GPSDeviceUpdate : ISerializeBinary
     {
-        /// <summary>
-        ///     Default Constants.
-        /// </summary>
-        public static IPAddress DEFAULT_SERVER = IPAddress.Any;
+        public string AuditTrail { get; set; }
 
-        public static int DEFAULT_PORT = 7999;
+        public GPSTrackerLocation Location { get; set; }
 
-        public static IPEndPoint DEFAULT_IP_END_POINT = new(DEFAULT_SERVER, DEFAULT_PORT);
-
-        private readonly CancellationTokenSource cts = new();
-
-        private readonly ConcurrentDictionary<string, Device> m_devices = new();
-        private Task m_purgingThread;
-
-        private DateTime m_RefreshDate = DateTime.MinValue;
+        public void SerializeBinary(CoreBinaryWriter writer)
+        {
+            writer.Write(AuditTrail ?? "");
+            writer.WriteObject(Location);
+        }
 
-        /// <summary>
-        ///     Local Variables Declaration.
-        /// </summary>
-        private TcpListener m_server;
+        public void DeserializeBinary(CoreBinaryReader reader)
+        {
+            AuditTrail = reader.ReadString();
+            Location = reader.ReadObject<GPSTrackerLocation>();
+        }
+    }
 
-        private Task m_serverThread;
-        private ArrayList m_socketListenersList;
-        private bool m_stopPurging;
-        private bool m_stopServer;
+    public class GPSUpdateQueue
+    {
+        public string QueuePath;
 
-        private readonly ConcurrentQueue<Tuple<GPSTrackerLocation, string>> m_updates = new();
-        private Task m_updatetask;
-        
-        public GPSEngine()
+        public GPSUpdateQueue(string queuePath)
         {
-            Init(DEFAULT_IP_END_POINT);
+            QueuePath = queuePath;
         }
 
-        public GPSEngine(IPAddress serverIP)
+        public void InitQueueFolder()
         {
-            Init(new IPEndPoint(serverIP, DEFAULT_PORT));
+            try
+            {
+                Directory.CreateDirectory(QueuePath);
+            }
+            catch (Exception e)
+            {
+                throw new Exception($"Could not create directory for device update queue: {QueuePath}", e);
+            }
         }
 
-        public GPSEngine(int port)
+        public int GetNumberOfItems()
         {
-            Init(new IPEndPoint(DEFAULT_SERVER, port));
+            return Directory.EnumerateFiles(QueuePath).Count();
         }
 
-        public GPSEngine(IPAddress serverIP, int port)
+        /// <summary>
+        /// Get the first (earliest) items of the directory.
+        /// </summary>
+        /// <returns>A list of (filename, update) tuples.</returns>
+        public IEnumerable<Tuple<string, GPSDeviceUpdate>> GetFirstItems()
         {
-            Init(new IPEndPoint(serverIP, port));
+            var files = Directory.EnumerateFiles(QueuePath).OrderBy(x => x);
+
+            foreach (var filename in files)
+            {
+                GPSDeviceUpdate? deviceUpdate = null;
+                try
+                {
+                    using var fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);
+                    deviceUpdate = Serialization.ReadBinary<GPSDeviceUpdate>(fileStream, BinarySerializationSettings.Latest);
+                }
+                catch
+                {
+                    // File is probably in use.
+                }
+                if(deviceUpdate is not null)
+                {
+                    yield return new Tuple<string, GPSDeviceUpdate>(filename, deviceUpdate);
+                }
+            }
         }
 
-        public GPSEngine(IPEndPoint ipNport)
+        public void QueueUpdate(GPSDeviceUpdate deviceUpdate)
         {
-            Init(ipNport);
+            var filename = Path.Combine(QueuePath, $"{DateTime.UtcNow.Ticks} - {deviceUpdate.Location.Tracker.ID}");
+
+            using var fileStream = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Write);
+            Serialization.WriteBinary(deviceUpdate, fileStream, BinarySerializationSettings.Latest);
         }
 
-        ~GPSEngine()
+        public void QueueUpdate(string auditTrail, GPSTrackerLocation location) => QueueUpdate(new GPSDeviceUpdate
         {
-            Stop();
+            AuditTrail = auditTrail,
+            Location = location
+        });
+    }
+
+    internal class GPSDeviceCache : ConcurrentDictionary<string, Device>
+    {
+        public void Refresh()
+        {
+            Logger.Send(LogType.Information, "", "Refreshing Tracker Cache");
+            var table = new Client<GPSTracker>().Query(
+                null,
+                new Columns<GPSTracker>(x => x.ID, x => x.DeviceID, x => x.Type.BatteryFormula));
+
+            Logger.Send(LogType.Information, "", string.Format("- Tracker Cache: {0} devices", table.Rows.Count));
+            Clear();
+            foreach (var row in table.Rows)
+            {
+                var formula = row.Get<GPSTracker, string?>(x => x.Type.BatteryFormula);
+                var expression = string.IsNullOrWhiteSpace(formula) ? null : new CoreExpression<GPSBatteryFormulaModel, double>(formula);
+                this[row.Get<GPSTracker, string>(x => x.DeviceID)] =
+                    new Device(row.Get<GPSTracker, Guid>(x => x.ID), DateTime.MinValue, expression);
+            }
         }
+    }
+
+    public class GPSEngine : Engine<GPSServerProperties>
+    {
+        private Listener<SigfoxHandler, SigfoxHandlerProperties> sigfoxListener;
+        private OEMListener oemListener;
+
+        private GPSDeviceCache DeviceCache = new();
+
+        private Timer RefreshDevicesTimer;
+
+        private Timer UpdateServerTimer;
+
+        private GPSUpdateQueue UpdateQueue;
 
         public override void Configure(Server server)
         {
             base.Configure(server);
+
+            UpdateQueue = new GPSUpdateQueue(Path.Combine(AppDataFolder, "device_queue"));
         }
 
-        /// <summary>
-        ///     Init method that create a server (TCP Listener) Object based on the
-        ///     IP Address and Port information that is passed in.
-        /// </summary>
-        /// <param name="ipNport"></param>
-        private void Init(IPEndPoint ipNport)
+        private void StartOEMListener()
         {
-            try
-            {
-                m_server = new TcpListener(ipNport);
-            }
-            catch (Exception e)
-            {
-                m_server = null;
-            }
+            if (Properties.ListenPort == 0)
+                throw new Exception("Error: OEM Listen Port not Specified\n");
 
-            m_updatetask = Task.Run(() =>
-            {
-                while (m_server != null && !cts.IsCancellationRequested)
-                    if (m_updates.Any())
-                        if (m_updates.TryDequeue(out var tuple))
-                        {
-                            Logger.Send(LogType.Information, "",
-                                string.Format("Updating Server ({0}): {1} - {2}", m_updates.Count, tuple.Item1.DeviceID, tuple.Item2));
-                            new Client<GPSTrackerLocation>().Save(tuple.Item1, tuple.Item2, (_, __) => { });
-                            Thread.Sleep(Properties.UpdateTimer);
-                        }
-            }, cts.Token);
-        }
+            Logger.Send(LogType.Information, "", "Starting OEM Listener on port " + Properties.ListenPort);
 
-        private static byte[] StringToByteArray(string hex)
-        {
-            var NumberChars = hex.Length;
-            var bytes = new byte[NumberChars / 2];
-            for (var i = 0; i < NumberChars; i += 2)
-                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
-            return bytes;
+            oemListener = new OEMListener(Properties.ListenPort, DeviceCache, UpdateQueue);
+            oemListener.Start();
+
+            Logger.Send(LogType.Information, "", "OEM Listener started on port " + Properties.ListenPort);
         }
 
-        private bool CheckConnection()
+        private void StartSigfoxListener()
         {
-            if (ClientFactory.UserGuid == Guid.Empty)
+            if (Properties.SigfoxListenPort == 0)
             {
-                // Wait for server connection
-                while (!Client.Ping())
-                {
-                    Logger.Send(LogType.Error, "", "Database server unavailable. Trying again in 30 seconds...");
-                    Task.Delay(30_000).Wait();
-                    Logger.Send(LogType.Information, "", "Retrying connection...");
-                }
-                ClientFactory.SetBypass();
+                Logger.Send(LogType.Information, "", "No Sigfox listen port specified\n");
+                return;
             }
 
-            if (DateTime.Now - m_RefreshDate > new TimeSpan(0, 5, 0))
+            sigfoxListener = new Listener<SigfoxHandler, SigfoxHandlerProperties>(new SigfoxHandlerProperties(DeviceCache, UpdateQueue));
+            sigfoxListener.InitPort((ushort)Properties.SigfoxListenPort);
+
+            Logger.Send(LogType.Information, "", "Starting Sigfox Listener on port " + Properties.SigfoxListenPort);
+
+            sigfoxListener.Start();
+
+            Logger.Send(LogType.Information, "", "Sigfox Listener started on port " + Properties.SigfoxListenPort);
+        }
+        
+        private void StartUpdateServerTask()
+        {
+            UpdateServerTimer = new Timer(Properties.UpdateTimer);
+            UpdateServerTimer.Elapsed += (o, e) => UpdateServer();
+            UpdateServerTimer.Start();
+        }
+
+        // List of (filename, update)
+        private Queue<Tuple<string, GPSDeviceUpdate>> LocationQueueCache = new();
+
+        private void GetLocationQueue(int nLocations)
+        {
+            LocationQueueCache.EnsureCapacity(LocationQueueCache.Count + nLocations);
+            foreach(var item in UpdateQueue.GetFirstItems().Take(nLocations))
             {
-                Logger.Send(LogType.Information, "", "Refreshing Tracker Cache");
-                var table = new Client<GPSTracker>().Query(
-                    null,
-                    new Columns<GPSTracker>(x => x.ID, x => x.DeviceID, x => x.Type.BatteryFormula)
-                );
-
-                m_RefreshDate = DateTime.Now;
-                Logger.Send(LogType.Information, "", string.Format("- Tracker Cache: {0} devices", table.Rows.Count));
-                m_devices.Clear();
-                foreach (var row in table.Rows)
-                {
-                    var formula = row.Get<GPSTracker, string?>(x => x.Type.BatteryFormula);
-                    var expression = string.IsNullOrWhiteSpace(formula) ? null : new CoreExpression<GPSBatteryFormulaModel, double>(formula);
-                    m_devices[row.Get<GPSTracker, string>(x => x.DeviceID)] =
-                        new Device(row.Get<GPSTracker, Guid>(x => x.ID), DateTime.MinValue, expression);
-                }
+                LocationQueueCache.Enqueue(item);
             }
-
-            return true;
         }
 
-        public void StartSigfoxListener()
+        private void UpdateServer()
         {
-            if(Properties.SigfoxListenPort == 0)
+            // Cache a set of fifty, so that we're not running baack and forth to the filesystem all the time.
+            if(LocationQueueCache.Count == 0)
             {
-                Logger.Send(LogType.Information, "", "No Sigfox listen port specified\n");
-                return;
+                GetLocationQueue(50);
             }
 
-            var listener = new Listener<SigfoxHandler, SigfoxHandlerProperties>(new SigfoxHandlerProperties(m_devices));
-            listener.InitPort((ushort)Properties.SigfoxListenPort);
+            if (LocationQueueCache.Count > 0)
+            {
+                var (filename, update) = LocationQueueCache.Dequeue();
 
-            Logger.Send(LogType.Information, "", "Starting Sigfox Listener on port " + Properties.SigfoxListenPort);
+                Logger.Send(LogType.Information, "",
+                    string.Format("Updating Server ({0}): {1} - {2}", UpdateQueue.GetNumberOfItems(), update.Location.DeviceID, update.AuditTrail));
+                new Client<GPSTrackerLocation>().Save(update.Location, update.AuditTrail, (_, exception) =>
+                {
+                    if (exception is not null)
+                    {
+                        Logger.Send(LogType.Error, "", $"Error saving GPS Tracker Location ({update.AuditTrail}): {CoreUtils.FormatException(exception)}");
+                    }
+                });
 
-            listener.Start();
+                try
+                {
+                    File.Delete(filename);
+                }
+                catch
+                {
+                    // Probably got deleted.
+                }
+            }
         }
 
-        /// <summary>
-        ///     Method that starts TCP/IP Server.
-        /// </summary>
         public override void Run()
         {
             if (string.IsNullOrWhiteSpace(Properties.Server))
@@ -229,171 +271,44 @@ namespace PRSServer
             ClientFactory.SetClientType(typeof(IPCClient<>), Platform.GPSEngine, Version, DatabaseServerProperties.GetPipeName(Properties.Server));
             CheckConnection();
 
-            DMFactory.Initialise(Properties.DumpFormat, Properties.DumpFile);
+            UpdateQueue.InitQueueFolder();
 
-            if (m_server != null)
-            {
-                // Create a ArrayList for storing SocketListeners before
-                // starting the server.
-                m_socketListenersList = new ArrayList();
-
-                // Start the Server and start the thread to listen client 
-                // requests.
-                m_server.Start();
-                m_serverThread = Task.Run(ServerThreadStart, cts.Token);
-
-                // Create a low priority thread that checks and deletes client
-                // SocktConnection objcts that are marked for deletion.
-                m_purgingThread = Task.Run(PurgingThreadStart, cts.Token);
-            }
+            // Refresh device cache and set up timer.
+            DeviceCache.Refresh();
+            RefreshDevicesTimer = new Timer(5 * 60 * 1000);
+            RefreshDevicesTimer.Elapsed += (o, e) => DeviceCache.Refresh();
+            RefreshDevicesTimer.Start();
+
+            DMFactory.Initialise(Properties.DumpFormat, Properties.DumpFile);
 
+            StartOEMListener();
             StartSigfoxListener();
+            StartUpdateServerTask();
         }
 
-        /// <summary>
-        ///     Method that stops the TCP/IP Server.
-        /// </summary>
-        public override void Stop()
+        private bool CheckConnection()
         {
-            if (m_server != null)
+            if (ClientFactory.UserGuid == Guid.Empty)
             {
-                // It is important to Stop the server first before doing
-                // any cleanup. If not so, clients might being added as
-                // server is running, but supporting data structures
-                // (such as m_socketListenersList) are cleared. This might
-                // cause exceptions.
-
-                cts.Cancel();
-
-                // Stop the TCP/IP Server.
-                m_stopServer = true;
-                m_server.Stop();
-
-                // Wait for one second for the the thread to stop.
-                //m_serverThread.Join(1000);
-
-                // If still alive; Get rid of the thread.
-                //if (m_serverThread.IsAlive)
-                //{
-                //   m_serverThread.Abort();
-                //}
-                m_serverThread = null;
-
-                m_stopPurging = true;
-                //m_purgingThread.Join(1000);
-                //if (m_purgingThread.IsAlive)
-                //{
-                //    m_purgingThread.Abort();
-                //}
-                //m_purgingThread = null;
-
-                // Free Server Object.
-                m_server = null;
-
-                while (m_updates.Any())
-                    if (m_updates.TryDequeue(out var tuple))
-                    {
-                        Logger.Send(LogType.Information, "",
-                            string.Format("Updating Server ({0}): {1} - {2}", m_updates.Count, tuple.Item1.DeviceID, tuple.Item2));
-                        new Client<GPSTrackerLocation>().Save(tuple.Item1, tuple.Item2, (_, __) => { });
-                        Thread.Sleep(2000);
-                    }
-
-                // Stop All clients.
-                StopAllSocketListers();
-            }
-        }
-
-
-        /// <summary>
-        ///     Method that stops all clients and clears the list.
-        /// </summary>
-        private void StopAllSocketListers()
-        {
-            foreach (GPSListener socketListener
-                     in m_socketListenersList)
-                socketListener.StopSocketListener();
-            // Remove all elements from the list.
-            m_socketListenersList.Clear();
-            m_socketListenersList = null;
-        }
-
-        /// <summary>
-        ///     TCP/IP Server Thread that is listening for clients.
-        /// </summary>
-        private void ServerThreadStart()
-        {
-            // Client Socket variable;
-            Socket clientSocket = null;
-            GPSListener socketListener = null;
-            while (!m_stopServer)
-                try
-                {
-                    if (!CheckConnection())
-                        return;
-
-                    // Wait for any client requests and if there is any 
-                    // request from any client accept it (Wait indefinitely).
-                    clientSocket = m_server.AcceptSocket();
-
-                    // Create a SocketListener object for the client.
-                    socketListener = new GPSListener(clientSocket, m_devices, m_updates);
-
-                    // Add the socket listener to an array list in a thread 
-                    // safe fashon.
-                    //Monitor.Enter(m_socketListenersList);
-                    lock (m_socketListenersList)
-                    {
-                        m_socketListenersList.Add(socketListener);
-                    }
-
-                    //Monitor.Exit(m_socketListenersList);
-                    // Start a communicating with the client in a different
-                    // thread.
-                    socketListener.StartSocketListener();
-                }
-                catch (SocketException se)
+                // Wait for server connection
+                while (!Client.Ping())
                 {
-                    m_stopServer = true;
+                    Logger.Send(LogType.Error, "", "Database server unavailable. Trying again in 30 seconds...");
+                    Task.Delay(30_000).Wait();
+                    Logger.Send(LogType.Information, "", "Retrying connection...");
                 }
+                ClientFactory.SetBypass();
+            }
+
+            return true;
         }
 
-        /// <summary>
-        ///     Thread method for purging Client Listeneres that are marked for
-        ///     deletion (i.e. clients with socket connection closed). This thead
-        ///     is a low priority thread and sleeps for 10 seconds and then check
-        ///     for any client SocketConnection obects which are obselete and
-        ///     marked for deletion.
-        /// </summary>
-        private void PurgingThreadStart()
+        public override void Stop()
         {
-            while (!m_stopPurging)
-            {
-                var deleteList = new ArrayList();
-
-                // Check for any clients SocketListeners that are to be
-                // deleted and put them in a separate list in a thread sage
-                // fashon.
-                //Monitor.Enter(m_socketListenersList);
-                lock (m_socketListenersList)
-                {
-                    foreach (GPSListener socketListener
-                             in m_socketListenersList)
-                        if (socketListener.IsMarkedForDeletion())
-                        {
-                            deleteList.Add(socketListener);
-                            socketListener.StopSocketListener();
-                        }
-
-                    // Delete all the client SocketConnection ojects which are
-                    // in marked for deletion and are in the delete list.
-                    for (var i = 0; i < deleteList.Count; ++i) m_socketListenersList.Remove(deleteList[i]);
-                }
-                //Monitor.Exit(m_socketListenersList);
-
-                deleteList = null;
-                Thread.Sleep(10000);
-            }
+            oemListener.Stop();
+            sigfoxListener.Stop();
+            UpdateServerTimer.Stop();
+            RefreshDevicesTimer.Stop();
         }
     }
-}
+}

+ 0 - 481
prs.server/Engines/GPS/GPSListener.cs

@@ -1,481 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Sockets;
-using System.Threading;
-using System.Windows.Media.Media3D;
-using Comal.Classes;
-using InABox.Core;
-using InABox.DigitalMatter;
-
-namespace PRSServer
-{
-    /// <summary>
-    ///     Summary description for TCPSocketListener.
-    /// </summary>
-   internal class GPSListener
-    {
-        private Thread m_clientListenerThread;
-
-        /// <summary>
-        ///     Variables that are accessed by other classes indirectly.
-        /// </summary>
-        private Socket m_clientSocket;
-
-        private DateTime m_currentReceiveDateTime;
-        private readonly ConcurrentDictionary<string, Device> m_devices = new();
-
-        /// <summary>
-        ///     Working Variables.
-        /// </summary>
-        private DateTime m_lastReceiveDateTime;
-
-        private bool m_markedForDeletion;
-        private bool m_stopClient;
-        private readonly ConcurrentQueue<Tuple<GPSTrackerLocation, string>> m_updates;
-
-
-        /// <summary>
-        ///     Client Socket Listener Constructor.
-        /// </summary>
-        /// <param name="clientSocket"></param>
-        public GPSListener(Socket clientSocket,
-            ConcurrentDictionary<string, Device> devices,
-            ConcurrentQueue<Tuple<GPSTrackerLocation, string>> updates)
-        {
-            m_clientSocket = clientSocket;
-            if (devices != null)
-                m_devices = devices;
-            m_updates = updates;
-        }
-
-        /// <summary>
-        ///     Client SocketListener Destructor.
-        /// </summary>
-        ~GPSListener()
-        {
-            StopSocketListener();
-        }
-
-        /// <summary>
-        ///     Method that starts SocketListener Thread.
-        /// </summary>
-        public void StartSocketListener()
-        {
-            if (m_clientSocket != null)
-            {
-                m_clientListenerThread =
-                    new Thread(SocketListenerThreadStart);
-
-                m_clientListenerThread.Start();
-            }
-        }
-
-        /// <summary>
-        ///     Thread method that does the communication to the client. This
-        ///     thread tries to receive from client and if client sends any data
-        ///     then parses it and again wait for the client data to come in a
-        ///     loop. The recieve is an indefinite time receive.
-        /// </summary>
-        private void SocketListenerThreadStart()
-        {
-            var _threshold = TimeSpan.FromMinutes(2);
-
-            var size = 0;
-            var buf = new byte[2048];
-            var consolidated = new List<byte>();
-
-            var m_product = "";
-            var m_serial = "";
-            var datarequests = new List<DMDataRequest>();
-
-            //var filename = Path.Combine(Logger.LogFolder,DateTime.Now.Ticks.ToString()+".txt");
-            //var writer = File.AppendText(filename);
-
-            m_lastReceiveDateTime = DateTime.Now;
-            m_currentReceiveDateTime = DateTime.Now;
-
-            var t = new Timer(CheckClientCommInterval,
-                null, 15000, 15000);
-
-            while (!m_stopClient)
-            {
-                var bClose = false;
-                try
-                {
-                    size = m_clientSocket.Receive(buf);
-                    m_currentReceiveDateTime = DateTime.Now;
-
-                    if (size > 0)
-                    {
-                        var buf2 = buf.Take(size).ToArray();
-                        //writer.WriteLine(BitConverter.ToString(buf2).Replace("-", string.Empty));
-                        consolidated.AddRange(buf2);
-                        try
-                        {
-                            while (consolidated.Count > 0)
-                            {
-                                DMMessage message = null;
-                                try
-                                {
-                                    message = DMFactory.ParseMessage(consolidated.ToArray());
-                                }
-                                catch (Exception e)
-                                {
-                                    Logger.Send(
-                                        LogType.Error,
-                                        Thread.CurrentThread.ManagedThreadId.ToString(),
-                                        string.Format("Unable to Parse Record: {0} ({1})", e.Message, BitConverter.ToString(consolidated.ToArray()))
-                                    );
-                                }
-
-                                if (message is DMHelloRequest hello)
-                                {
-                                    m_product = DMFactory.GetDeviceName(hello.ProductID);
-                                    m_serial = hello.SerialNumber.ToString();
-                                    Logger.Send(LogType.Information, m_serial, string.Format("Hello {0} ({1})", m_product, m_serial));
-                                    var ack = new DMHelloResponse();
-                                    m_clientSocket.Send(ack.Encode());
-                                }
-                                else if (message is DMDataRequest)
-                                {
-                                    //Logger.Send(LogType.Information, m_serial, String.Format("Data: {0}", String.Join(":",consolidated.Select(x => x.ToString("X2")))));
-                                    var data = message as DMDataRequest;
-                                    Logger.Send(LogType.Information, m_serial, string.Format("{0} DataRecords Received", data.Records.Length));
-                                    var iRecord = 1;
-                                    foreach (var record in data.Records)
-                                    {
-                                        Logger.Send(LogType.Information, m_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, m_serial,
-                                                string.Format("  [{0}] {1}: {2}", field.IsValid() ? "X" : " ", DMFactory.GetFieldName(field.Type),
-                                                    field));
-                                    }
-
-                                    datarequests.Add(data);
-                                    // Update the Server Here
-                                }
-                                else if (message is DMConfirmRequest)
-                                {
-                                    Logger.Send(LogType.Information, m_serial, string.Format("Goodbye {0} ({1})", m_product, m_serial));
-
-                                    var updates = new List<GPSTrackerLocation>();
-                                    foreach (var data in datarequests)
-                                    foreach (var record in data.Records)
-                                    {
-                                        var gps = record.Fields.FirstOrDefault(x => x is DMGPSField && x.IsValid()) as DMGPSField;
-                                        if (m_devices.ContainsKey(m_serial))
-                                        {
-                                            if (gps != null)
-                                            {
-                                                if (record.TimeStamp != 0 && gps.Latitude != 0 && gps.Longitude != 0)
-                                                {
-                                                    if (!gps.StatusFlags().Any(x => x == GPSStatus.NoSignal))
-                                                    {
-                                                        var timestamp = record.TimeStampToDateTime(record.TimeStamp);
-                                                        var age = timestamp - m_devices[m_serial].TimeStamp;
-                                                        if (age > _threshold)
-                                                        {
-                                                            var device = m_devices[m_serial];
-
-                                                            var location = new GPSTrackerLocation();
-                                                            location.DeviceID = m_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;
-                                                            updates.Add(location);
-
-                                                            var analoguedata =
-                                                                record.Fields.FirstOrDefault(x =>
-                                                                    x is DMAnalogueDataField16) as DMAnalogueDataField16;
-                                                            if (analoguedata != null)
-                                                                location.BatteryLevel = analoguedata.BatteryStrength
-                                                                        ?? device.CalculateBatteryLevel(analoguedata.InternalVoltage);
-                                                        }
-                                                        else
-                                                        {
-                                                            Logger.Send(LogType.Information, m_serial,
-                                                                string.Format("- Skipping: Recent Update ({0}) {1:mm\\:ss}", m_serial, age));
-                                                        }
-                                                    }
-                                                    else
-                                                    {
-                                                        Logger.Send(LogType.Information, m_serial,
-                                                            string.Format("- Skipping: Invalid Signal ({0})", m_serial));
-                                                    }
-
-                                                    var taglists = record.Fields.Where(x => x is DMBluetoothTagList);
-                                                    foreach (DMBluetoothTagList taglist in taglists)
-                                                    foreach (var item in taglist.Items.Where(x => x.LogReason != 2))
-                                                    {
-                                                        var tagid = item.Tag.ID();
-
-
-                                                        if (!m_devices.ContainsKey(tagid) && tagid.Length == 17 && tagid.Split(':').Length == 6)
-                                                        {
-                                                            var truncated = tagid.Substring(0, 15);
-                                                            var newtag = m_devices.Keys.FirstOrDefault(x => x.StartsWith(truncated));
-                                                            Logger.Send(LogType.Information, m_serial,
-                                                                string.Format("- Truncating BT Tag: {0} -> {1} -> {2}", tagid, truncated, newtag));
-                                                            if (!string.IsNullOrWhiteSpace(newtag))
-                                                                tagid = newtag;
-                                                        }
-
-                                                        if (m_devices.ContainsKey(tagid))
-                                                        {
-                                                            var timestamp = record.TimeStampToDateTime(record.TimeStamp);
-                                                            var age = timestamp - m_devices[tagid].TimeStamp;
-                                                            if (age > _threshold)
-                                                            {
-                                                                var device = m_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 (item.Tag is DMGuppyBluetoothTag guppy)
-                                                                {
-                                                                    btloc.BatteryLevel = device.CalculateBatteryLevel(guppy.BatteryVoltage);
-                                                                    //guppy.BatteryVoltage * 5F / 3F;
-                                                                }
-                                                                else if (item.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;
-                                                                }
-
-                                                                updates.Add(btloc);
-                                                            }
-                                                            else
-                                                            {
-                                                                Logger.Send(LogType.Information, m_serial,
-                                                                    string.Format("- Skipping: Recent Update ({0}) {1:mm\\:ss}", tagid, age));
-                                                            }
-                                                        }
-                                                        else
-                                                        {
-                                                            Logger.Send(LogType.Information, m_serial,
-                                                                string.Format("- Skipping: Unknown Tag ({0})", tagid));
-                                                        }
-                                                    }
-
-                                                    var tags = record.Fields.Where(x => x is DMBluetoothTagData);
-                                                    foreach (DMBluetoothTagData tag in tags)
-                                                        if (tag.LogReason != 2 && tag.TimeStamp != 0 && tag.Latitude != 0 && tag.Longitude != 0)
-                                                        {
-                                                            var tagid = tag.Tag.ID();
-
-                                                            if (!m_devices.ContainsKey(tagid) && tagid.Length == 17 && tagid.Split(':').Length == 6)
-                                                            {
-                                                                var truncated = tagid.Substring(0, 15);
-                                                                var newtag = m_devices.Keys.FirstOrDefault(x => x.StartsWith(truncated));
-                                                                Logger.Send(LogType.Information, m_serial,
-                                                                    string.Format("- Truncating BT Tag: {0} -> {1} -> {2}", tagid, truncated,
-                                                                        newtag));
-                                                                if (!string.IsNullOrWhiteSpace(newtag))
-                                                                    tagid = newtag;
-                                                            }
-
-                                                            if (m_devices.ContainsKey(tagid))
-                                                            {
-                                                                var timestamp = record.TimeStampToDateTime(record.TimeStamp);
-                                                                var age = timestamp - m_devices[tagid].TimeStamp;
-                                                                if (age > _threshold)
-                                                                    {
-                                                                    var device = m_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;
-                                                                    updates.Add(btloc);
-                                                                    
-                                                                    if (tag.Tag is DMGuppyBluetoothTag guppy)
-                                                                    {
-                                                                        btloc.BatteryLevel = device.CalculateBatteryLevel(guppy.BatteryVoltage);
-                                                                        //guppy.BatteryVoltage * 5F / 3F;
-                                                                    }
-                                                                    else if (tag.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;
-                                                                    }
-
-                                                                }
-                                                                else
-                                                                {
-                                                                    Logger.Send(LogType.Information, m_serial,
-                                                                        string.Format("- Skipping: Recent Update ({0}) {1:mm\\:ss}", tagid, age));
-                                                                }
-                                                            }
-                                                            else
-                                                            {
-                                                                Logger.Send(LogType.Information, m_serial,
-                                                                    string.Format("- Skipping: Unknown Tag ({0})", tagid));
-                                                            }
-                                                        }
-                                                }
-                                                else
-                                                {
-                                                    Logger.Send(LogType.Information, m_serial,
-                                                        string.Format("- Skipping: Invalid GPS Data ({0}) {1}{2}{3}", m_serial,
-                                                            gps.TimeStamp == 0 ? "Bad TimeStamp " : "", gps.Latitude == 0 ? "Bad Latitude " : "",
-                                                            gps.Longitude == 0 ? "Bad Longitude " : "").Trim());
-                                                }
-                                            }
-                                            else
-                                            {
-                                                Logger.Send(LogType.Information, m_serial,
-                                                    string.Format("- Skipping: Missing GPS Data ({0})", m_serial));
-                                            }
-                                        }
-                                        else
-                                        {
-                                            Logger.Send(LogType.Information, m_serial, string.Format("- Skipping: Unknown Device ({0})", m_serial));
-                                        }
-                                    }
-
-                                    if (updates.Any())
-                                    {
-                                        Logger.Send(LogType.Information, m_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, m_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 = m_devices[update.DeviceID];
-                                            m_devices[update.DeviceID] =
-                                                new Device(oldDevice.ID, update.Location.Timestamp, oldDevice.BatteryFormula);
-                                        }
-
-                                        foreach (var update in updates)
-                                            m_updates.Enqueue(new Tuple<GPSTrackerLocation, string>(update,
-                                                string.Format("Updated by {0} ({1})", m_product, m_serial)));
-
-                                        //new Client<GPSTrackerLocation>().Save(
-                                        //    updates,
-                                        //    String.Format("Updated by {0} ({1})", m_product, m_serial),
-                                        //    (locations, errors) =>
-                                        //    {
-                                        //        //if (locations != null)
-                                        //        //{
-                                        //        //    foreach (var location in locations)
-                                        //        //    {
-                                        //        //        if (m_devices.ContainsKey(location.DeviceID))
-                                        //        //        m_devices[location.DeviceID] = location.Tracker.ID;
-                                        //        //    }
-                                        //        //}
-                                        //    }
-                                        //);
-                                    }
-
-                                    var ack = new DMConfirmResponse();
-                                    ack.Status = 1;
-                                    m_clientSocket.Send(ack.Encode());
-
-                                    //bClose = true;
-                                }
-
-                                consolidated.RemoveRange(0, message.CheckSum + 5);
-                            }
-                        }
-                        catch (Exception e)
-                        {
-                            Logger.Send(LogType.Error, Thread.CurrentThread.ManagedThreadId.ToString(), e.Message + "\n" + e.StackTrace);
-                        }
-                    }
-
-
-                    else
-                    {
-                        bClose = true;
-                    }
-                }
-                catch (SocketException se)
-                {
-                    bClose = true;
-                }
-
-                if (bClose)
-                {
-                    //writer.Flush();
-                    //writer.Close();
-                    //writer.Dispose();
-                    //writer = null;
-
-                    m_stopClient = true;
-                    m_markedForDeletion = true;
-                }
-            }
-
-            t.Change(Timeout.Infinite, Timeout.Infinite);
-            t = null;
-        }
-
-
-        /// <summary>
-        ///     Method that stops Client SocketListening Thread.
-        /// </summary>
-        public void StopSocketListener()
-        {
-            if (m_clientSocket != null)
-            {
-                m_stopClient = true;
-                m_clientSocket.Close();
-
-                // Wait for one second for the the thread to stop.
-                m_clientListenerThread.Join(1000);
-
-                // If still alive; Get rid of the thread.
-                if (m_clientListenerThread.IsAlive)
-                {
-                    Logger.Send(LogType.Error, "", "Thread didn't die in time.");
-                }
-                    //m_clientListenerThread.Abort();
-                m_clientListenerThread = null;
-                m_clientSocket = null;
-                m_markedForDeletion = true;
-            }
-        }
-
-        /// <summary>
-        ///     Method that returns the state of this object i.e. whether this
-        ///     object is marked for deletion or not.
-        /// </summary>
-        /// <returns></returns>
-        public bool IsMarkedForDeletion()
-        {
-            return m_markedForDeletion;
-        }
-
-        /// <summary>
-        ///     Method that checks whether there are any client calls for the
-        ///     last 15 seconds or not. If not this client SocketListener will
-        ///     be closed.
-        /// </summary>
-        /// <param name="o"></param>
-        private void CheckClientCommInterval(object o)
-        {
-            if (m_lastReceiveDateTime.Equals(m_currentReceiveDateTime))
-                StopSocketListener();
-            else
-                m_lastReceiveDateTime = m_currentReceiveDateTime;
-        }
-    }
-}

+ 447 - 0
prs.server/Engines/GPS/OEMConnection.cs

@@ -0,0 +1,447 @@
+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
+{
+    /// <summary>
+    /// Represents one conversation between OEM and our GPS Engine.
+    /// </summary>
+    internal class OEMConnection
+    {
+        private TcpClient Client;
+        private NetworkStream Stream;
+
+        private byte[] Buffer;
+        private List<byte> 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<DMDataRequest> 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)
+        {
+            if (gps.StatusFlags().Any(x => x == GPSStatus.NoSignal))
+            {
+                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<DMAnalogueDataField16>().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<GPSTrackerLocation> 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<DMBluetoothTagList>())
+                            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<DMBluetoothTagData>())
+                            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<GPSTrackerLocation>();
+            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());
+            }
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <remarks>
+        /// Read occurs asynchronously, so essentially this method spins off a read thread and then returns.
+        /// If we call <see cref="Stop"/> before data comes in, the read does not occur.
+        /// </remarks>
+        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<byte>();
+
+            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;
+                    }
+                }
+            }
+        }
+    }
+}

+ 97 - 0
prs.server/Engines/GPS/OEMListener.cs

@@ -0,0 +1,97 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.DigitalMatter;
+using NPOI.HSSF.Record.CF;
+using PRSServer.Engines;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Media.Media3D;
+
+namespace PRSServer
+{
+    internal class OEMListener
+    {
+        private TcpListener listener;
+
+        private GPSDeviceCache Cache;
+        private GPSUpdateQueue Queue;
+
+        private object connectionsLock = new object();
+        private List<OEMConnection> Connections = new();
+
+        public int Port { get; set; }
+
+        public OEMListener(int port, GPSDeviceCache cache, GPSUpdateQueue queue)
+        {
+            Port = port;
+            Cache = cache;
+            Queue = queue;
+        }
+
+        public void Start()
+        {
+            listener = new TcpListener(IPAddress.Any, Port);
+            listener.Start();
+
+            AcceptClient();
+        }
+
+        private void AcceptClient()
+        {
+            Logger.Send(LogType.Information, "", "Waiting for OEM connection.");
+            listener.AcceptTcpClientAsync().ContinueWith(t =>
+            {
+                try
+                {
+                    Logger.Send(LogType.Information, "", "OEM Connection opened; {Connections.Count} connections open.");
+                    var connection = new OEMConnection(t.Result, Cache, Queue);
+                    lock (connectionsLock)
+                    {
+                        Connections.Add(connection);
+                    }
+
+                    connection.OnCompletion = () =>
+                    {
+                        // When the connection finishes, we should remove it from our list.
+                        lock (connectionsLock)
+                        {
+                            Logger.Send(LogType.Information, "", $"OEM Connection closed; {Connections.Count} connections still open.");
+                            Connections.Remove(connection);
+                        }
+                    };
+
+                    connection.Run();
+
+                    // Accept a new client.
+                    AcceptClient();
+                }
+                catch (Exception e)
+                {
+                    Logger.Send(LogType.Error, "", CoreUtils.FormatException(e));
+                }
+            }, TaskContinuationOptions.OnlyOnRanToCompletion);
+        }
+
+        public void Stop()
+        {
+            listener.Stop();
+            
+            lock (connectionsLock)
+            {
+                // Running in parallel because each connection can take a second to stop.
+                Parallel.ForEach(Connections, connection =>
+                {
+                    connection.Stop();
+                });
+                Connections.Clear();
+            }
+        }
+    }
+}

+ 9 - 3
prs.server/Engines/GPS/SigfoxListener.cs

@@ -24,9 +24,12 @@ namespace PRSServer.Engines
     {
         public ConcurrentDictionary<string, Device> Devices;
 
-        public SigfoxHandlerProperties(ConcurrentDictionary<string, Device> devices)
+        public GPSUpdateQueue Queue;
+
+        public SigfoxHandlerProperties(ConcurrentDictionary<string, Device> devices, GPSUpdateQueue queue)
         {
             Devices = devices;
+            Queue = queue;
         }
     }
 
@@ -46,6 +49,8 @@ namespace PRSServer.Engines
     {
         private ConcurrentDictionary<string, Device> Devices;
 
+        private GPSUpdateQueue Queue;
+
         private IResponseBuilder HandleSigfox(IRequest request)
         {
             if(request.Content == null) return request.Respond().Status(ResponseStatus.BadRequest);
@@ -83,14 +88,14 @@ namespace PRSServer.Engines
                 : DateTime.Now;
 
             location.Speed = sigfoxLocation.Speed;
-            // For some reaon the 
+
             location.BatteryLevel = device.CalculateBatteryLevel(sigfoxLocation.BatteryLevel); // sigfoxLocation.BatteryLevel / 5.25F * 100.0F
             location.InTrip = sigfoxLocation.InTrip;
             location.LastFixFailed = sigfoxLocation.LastFixFailed;
 
             Logger.Send(LogType.Information, "", $"Sigfox ({location.DeviceID}) Lat: {sigfoxLocation.Latitude}, Long: {sigfoxLocation.Longitude}");
 
-            new Client<GPSTrackerLocation>().Save(location, "Updated by Sigfox Platform");
+            Queue.QueueUpdate("Updated by Sigfox Platform", location);
 
             return request.Respond().Status(ResponseStatus.OK);
         }
@@ -128,6 +133,7 @@ namespace PRSServer.Engines
         public override void Init(SigfoxHandlerProperties properties)
         {
             Devices = properties.Devices;
+            Queue = properties.Queue;
         }
     }
 

+ 1 - 1
prs.server/Engines/Scheduler/ScheduleEngine.cs

@@ -42,7 +42,7 @@ namespace PRSServer
             catch (Exception ex)
             {
                 Logger.Send(LogType.Error, "", "Error: " + ex.Message + "\n" + ex.StackTrace);
-                throw ex;
+                throw;
             }
         }
 

+ 0 - 3
prs.server/Services/PRSService.cs

@@ -41,9 +41,6 @@ namespace PRSServer
             else
                 _servicename = serviceName;
             ServiceName = _servicename;
-            
-
-            
         }
 
         private string GetServiceName()