private void ReceiveEnd(IAsyncResult ar)
        {
            // Retrieve state
            var state = (ConnectionState)ar.AsyncState;

            // Complete read
            Int32 chunkLength;

            try {
                chunkLength = state.Connection.EndReceive(ar);
            } catch (ObjectDisposedException) // Occurs during in-flight disposal - we need to catch it for a graceful shutdown
            {
                return;                       // Abort read
            } catch (SocketException ex)      // Occurs when there is a connection error
            {
                // Report error
                RaiseDeviceError(state, ex.Message, false);
                return;                 // Abort receive
            }

            // If the expected number of bytes hasn't arrived yet, wait for more bytes
            if (state.ReceiveBufferExpected != state.ReceiveBufferUsed)
            {
                // Receive more bytes
                ReceiveStart(state);
                return;
            }

            // If header hasn't been processed yet, process it
            if (null == state.ReceiveReceiveMessageType)
            {
                // Yes, this could have been in the switch below, but it made for some funky reading
                // Check sync bytes
                if (state.ReceiveBuffer[0] != 0x02 || state.ReceiveBuffer[1] != 0x55)
                {
                    RaiseDeviceError(state, $"Incorrect sync bytes encountered ({state.ReceiveBuffer[0]},{state.ReceiveBuffer[1]}).", true);
                    return;
                }

                // Decode message type
                state.ReceiveReceiveMessageType = (ReceiveMessageTypes)state.ReceiveBuffer[2];

                // Decode message length and increase the number of bytes expected
                var messageLength = BitConverter.ToUInt16(state.ReceiveBuffer, 3);
                state.ReceiveBufferExpected += messageLength;

                // Receive more bytes
                ReceiveStart(state);
                return;
            }

            switch (state.ReceiveReceiveMessageType.Value)
            {
            case ReceiveMessageTypes.Hello:                     // Device is providing basic information about it
                state.Device = Device.Parse(state.ReceiveBuffer, 5);

                state.Connection.Send(new Byte[] { 0x02, 0x55 });                // Sync bytes
                state.Connection.Send((Byte)TransmitMessageTypes.HelloResponse); // Message type
                state.Connection.Send((UInt16)8);                                // Message length
                state.Connection.Send(TelematicsTime.Encode(DateTime.UtcNow));   // Time
                state.Connection.Send((UInt32)0);                                // Flags TODO: how to tell device to redirect to OEM?

                OnDeviceConnected?.Invoke(this, new OnDeviceConnectedArgs()
                {
                    RemoteAddressString = state.RemoteAddressString,
                    Device = state.Device
                });

                break;

            case ReceiveMessageTypes.Records:                     // Device is providing records
                // It seems that MESSAGE contains multiple RECORDS
                //   which contain multiple FIELDS
                //     which contains multiple things which I'm going to call ATTRIBUTES
                // Attributes are the actual data.
                //
                // In summary: MESSAGE => RECORDS => FIELDS => ATTRIBUTES

                var position = 5;
                var records  = new List <Record>();

                while (position < state.ReceiveBufferExpected)
                {
                    records.Add(Record.Parse(state.ReceiveBuffer, ref position));
                }

                OnRecordsReceived?.Invoke(this, new OnRecordsReceivedArgs()
                {
                    RemoteAddressString = state.RemoteAddressString,
                    Device  = state.Device,
                    Records = records
                });
                break;

            case ReceiveMessageTypes.CommitRequest:                               // Device asking us to confirm that we've successfully received and stored records
                // Send confirmation // TODO: When would we not confirm a commit?
                state.Connection.Send(new Byte[] { 0x02, 0x55 });                 // Sync bytes
                state.Connection.Send((Byte)TransmitMessageTypes.CommitResponse); // Message type
                state.Connection.Send((UInt16)1);                                 // Message length
                state.Connection.Send((Byte)1);                                   // Success
                break;

            case ReceiveMessageTypes.VersionData:                                               // Device providing version information (no idea what this is for!!)
                RaiseDeviceError(state, $"Version information received and discarded.", false); // TODO: do something with the information
                break;

            case ReceiveMessageTypes.TimeRequest:                               // Device asking for the current time
                state.Connection.Send(new Byte[] { 0x02, 0x55 });               // Sync bytes
                state.Connection.Send((Byte)TransmitMessageTypes.TimeResponse); // Message type
                state.Connection.Send((UInt16)4);                               // Message length
                state.Connection.Send(TelematicsTime.Encode(DateTime.UtcNow));  // Current time
                break;

            case ReceiveMessageTypes.AsyncMessageResponse:                                          // Device is giving responses to async messages, which is not currently implemented, so should not be recieved
                RaiseDeviceError(state, $"Message async responses received and discarded.", false); // TODO: do something with the information
                break;

            default:
                RaiseDeviceError(state, $"Unsupported message type received ({state.ReceiveReceiveMessageType.Value}). Message discarded.", false);
                break;
            }

            // Receive more
            ResetState(state);
            ReceiveStart(state);
        }
 public void DecodeOne()
 {
     Assert.Equal(new DateTime(2013, 01, 01, 0, 0, 1, DateTimeKind.Utc), TelematicsTime.Decode(1));
 }
 public void EncodeZero()
 {
     Assert.Equal((UInt32)0, TelematicsTime.Encode(new DateTime(2013, 01, 01, 0, 0, 0, DateTimeKind.Utc)));
 }
 public void DecodeZero()
 {
     Assert.Equal(new DateTime(2013, 01, 01, 0, 0, 0, DateTimeKind.Utc), TelematicsTime.Decode(0));
 }