Пример #1
0
        public async Task OTAA_Confirmed_Message_Should_Send_Data_To_IotHub_Update_FcntUp_And_Return_DownstreamMessage()
        {
            var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: this.ServerConfiguration.GatewayID));
            var payload         = simulatedDevice.CreateConfirmedDataUpMessage("1234");

            // Create Rxpk
            var rxpk = payload.SerializeUplink(simulatedDevice.AppSKey, simulatedDevice.NwkSKey).Rxpk[0];

            var loraDeviceClient = new Mock <ILoRaDeviceClient>();
            var loraDevice       = TestUtils.CreateFromSimulatedDevice(simulatedDevice, loraDeviceClient.Object);

            loraDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull <LoRaDeviceTelemetry>(), null))
            .ReturnsAsync(true);

            var payloadDecoder = new Mock <ILoRaPayloadDecoder>();

            this.LoRaDeviceRegistry.Setup(x => x.GetDeviceForPayloadAsync(It.IsAny <LoRaTools.LoRaMessage.LoRaPayloadData>()))
            .ReturnsAsync(loraDevice);

            // Setup frame counter strategy
            this.FrameCounterUpdateStrategyFactory.Setup(x => x.GetSingleGatewayStrategy())
            .Returns(new SingleGatewayFrameCounterUpdateStrategy());

            // Frame counter will be asked to save changes
            this.FrameCounterUpdateStrategy.Setup(x => x.SaveChangesAsync(loraDevice)).ReturnsAsync(true);

            // Send to message processor
            var messageProcessor = new MessageProcessor(
                this.ServerConfiguration,
                this.LoRaDeviceRegistry.Object,
                this.FrameCounterUpdateStrategyFactory.Object,
                payloadDecoder.Object);

            var actual = await messageProcessor.ProcessMessageAsync(rxpk);

            // Expectations
            // 1. Message was sent to IoT Hub
            loraDeviceClient.VerifyAll();

            // 2. Single gateway frame counter strategy was used
            this.FrameCounterUpdateStrategyFactory.VerifyAll();

            // 3. Return is downstream message
            Assert.NotNull(actual);
            Assert.IsType <DownlinkPktFwdMessage>(actual);
            var downlinkMessage = (DownlinkPktFwdMessage)actual;
            var payloadDataDown = new LoRaPayloadData(Convert.FromBase64String(downlinkMessage.Txpk.Data));

            Assert.Equal(payloadDataDown.DevAddr.ToArray(), LoRaTools.Utils.ConversionHelper.StringToByteArray(loraDevice.DevAddr));
            Assert.False(payloadDataDown.IsConfirmed());
            Assert.Equal(LoRaMessageType.UnconfirmedDataDown, payloadDataDown.LoRaMessageType);

            // 4. Frame counter up was updated
            Assert.Equal(1, loraDevice.FCntUp);

            // 5. Frame counter down was incremented
            Assert.Equal(1, loraDevice.FCntDown);
            Assert.Equal(1, MemoryMarshal.Read <ushort>(payloadDataDown.Fcnt.Span));
        }
        public async Task OTAA_Confirmed_With_Cloud_To_Device_Message_Returns_Downstream_Message()
        {
            const int PayloadFcnt           = 10;
            const int InitialDeviceFcntUp   = 9;
            const int InitialDeviceFcntDown = 20;

            var simulatedDevice = new SimulatedDevice(
                TestDeviceInfo.CreateABPDevice(1, gatewayID: this.ServerConfiguration.GatewayID),
                frmCntUp: InitialDeviceFcntUp,
                frmCntDown: InitialDeviceFcntDown);

            var loraDeviceClient = new Mock <ILoRaDeviceClient>(MockBehavior.Strict);
            var loraDevice       = TestUtils.CreateFromSimulatedDevice(simulatedDevice, loraDeviceClient.Object);

            loraDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull <LoRaDeviceTelemetry>(), null))
            .ReturnsAsync(true);

            loraDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsNotNull <TwinCollection>()))
            .ReturnsAsync(true);

            var cloudToDeviceMessage = new Message(Encoding.UTF8.GetBytes("c2d"));

            cloudToDeviceMessage.Properties[MessageProcessor.FPORT_MSG_PROPERTY_KEY] = "1";
            loraDeviceClient.SetupSequence(x => x.ReceiveAsync(It.IsAny <TimeSpan>()))
            .ReturnsAsync(cloudToDeviceMessage)
            .ReturnsAsync((Message)null);     // 2nd cloud to device message does not return anything

            loraDeviceClient.Setup(x => x.CompleteAsync(cloudToDeviceMessage))
            .ReturnsAsync(true);

            var payloadDecoder = new Mock <ILoRaPayloadDecoder>();

            this.LoRaDeviceRegistry.Setup(x => x.GetDeviceForPayloadAsync(It.IsAny <LoRaTools.LoRaMessage.LoRaPayloadData>()))
            .ReturnsAsync(loraDevice);

            // Setup frame counter strategy
            this.FrameCounterUpdateStrategyFactory.Setup(x => x.GetSingleGatewayStrategy())
            .Returns(new SingleGatewayFrameCounterUpdateStrategy());

            // Send to message processor
            var messageProcessor = new MessageProcessor(
                this.ServerConfiguration,
                this.LoRaDeviceRegistry.Object,
                this.FrameCounterUpdateStrategyFactory.Object,
                payloadDecoder.Object);

            var payload = simulatedDevice.CreateConfirmedDataUpMessage("1234", fcnt: PayloadFcnt);
            var rxpk    = payload.SerializeUplink(simulatedDevice.AppSKey, simulatedDevice.NwkSKey).Rxpk[0];
            var actual  = await messageProcessor.ProcessMessageAsync(rxpk);

            // Expectations
            // 1. Message was sent to IoT Hub
            loraDeviceClient.VerifyAll();

            // 2. Single gateway frame counter strategy was used
            this.FrameCounterUpdateStrategyFactory.VerifyAll();

            // 3. Return is downstream message
            Assert.NotNull(actual);
            Assert.IsType <DownlinkPktFwdMessage>(actual);
            var downlinkMessage = (DownlinkPktFwdMessage)actual;
            var payloadDataDown = new LoRaPayloadData(Convert.FromBase64String(downlinkMessage.Txpk.Data));

            payloadDataDown.PerformEncryption(loraDevice.AppSKey);
            Assert.Equal(payloadDataDown.DevAddr.ToArray(), LoRaTools.Utils.ConversionHelper.StringToByteArray(loraDevice.DevAddr));
            Assert.False(payloadDataDown.IsConfirmed());
            Assert.Equal(LoRaMessageType.UnconfirmedDataDown, payloadDataDown.LoRaMessageType);

            // 4. Frame counter up was updated
            Assert.Equal(PayloadFcnt, loraDevice.FCntUp);

            // 5. Frame counter down is updated
            Assert.Equal(InitialDeviceFcntDown + 1, loraDevice.FCntDown);
            Assert.Equal(InitialDeviceFcntDown + 1, payloadDataDown.GetFcnt());

            // 6. Frame count has no pending changes
            Assert.False(loraDevice.HasFrameCountChanges);
        }
        public async Task When_Device_Prefers_Second_Window_Should_Send_Downstream_In_Second_Window()
        {
            const int PayloadFcnt           = 10;
            const int InitialDeviceFcntUp   = 9;
            const int InitialDeviceFcntDown = 20;

            var simulatedDevice = new SimulatedDevice(
                TestDeviceInfo.CreateABPDevice(1, gatewayID: this.ServerConfiguration.GatewayID),
                frmCntUp: InitialDeviceFcntUp,
                frmCntDown: InitialDeviceFcntDown);

            var devAddr = simulatedDevice.DevAddr;
            var devEUI  = simulatedDevice.DevEUI;

            var loraDeviceClient = new Mock <ILoRaDeviceClient>(MockBehavior.Strict);

            // Will get twin to initialize LoRaDevice
            var deviceTwin = TestUtils.CreateABPTwin(
                simulatedDevice,
                desiredProperties: new Dictionary <string, object>
            {
                { TwinProperty.PreferredWindow, 2 }
            });

            loraDeviceClient.Setup(x => x.GetTwinAsync()).ReturnsAsync(deviceTwin);

            loraDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull <LoRaDeviceTelemetry>(), null))
            .ReturnsAsync(true);

            loraDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsNotNull <TwinCollection>()))
            .ReturnsAsync(true);

            var cloudToDeviceMessage = new Message(Encoding.UTF8.GetBytes("c2d"));

            cloudToDeviceMessage.Properties[MessageProcessor.FPORT_MSG_PROPERTY_KEY] = "1";
            loraDeviceClient.SetupSequence(x => x.ReceiveAsync(It.IsAny <TimeSpan>()))
            .ReturnsAsync(cloudToDeviceMessage)
            .Returns(this.EmptyAdditionalMessageReceiveAsync);     // 2nd cloud to device message does not return anything

            loraDeviceClient.Setup(x => x.CompleteAsync(cloudToDeviceMessage))
            .ReturnsAsync(true);

            var payloadDecoder = new Mock <ILoRaPayloadDecoder>();

            var loRaDeviceAPI = new Mock <LoRaDeviceAPIServiceBase>();

            loRaDeviceAPI.Setup(x => x.SearchByDevAddrAsync(devAddr))
            .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEUI, "adad").AsList()));

            var loRaDeviceRegistry = new LoRaDeviceRegistry(this.ServerConfiguration, new MemoryCache(new MemoryCacheOptions()), loRaDeviceAPI.Object, new TestLoRaDeviceFactory(loraDeviceClient.Object));

            // Send to message processor
            var messageProcessor = new MessageProcessor(
                this.ServerConfiguration,
                loRaDeviceRegistry,
                new LoRaDeviceFrameCounterUpdateStrategyFactory(this.ServerConfiguration.GatewayID, loRaDeviceAPI.Object),
                new LoRaPayloadDecoder());

            var payload = simulatedDevice.CreateUnconfirmedDataUpMessage("1234", fcnt: PayloadFcnt);
            var rxpk    = payload.SerializeUplink(simulatedDevice.AppSKey, simulatedDevice.NwkSKey).Rxpk[0];
            var actual  = await messageProcessor.ProcessMessageAsync(rxpk);

            // Expectations
            // 1. Message was sent to IoT Hub
            loraDeviceClient.VerifyAll();

            // 2. Single gateway frame counter strategy was used
            this.FrameCounterUpdateStrategyFactory.VerifyAll();

            // 3. Return is downstream message
            Assert.NotNull(actual);
            Assert.IsType <DownlinkPktFwdMessage>(actual);
            var downlinkMessage = (DownlinkPktFwdMessage)actual;
            var euRegion        = RegionFactory.CreateEU868Region();

            // Ensure we are using second window frequency
            Assert.Equal(euRegion.RX2DefaultReceiveWindows.frequency, actual.Txpk.Freq);

            // Ensure we are using second window datr
            Assert.Equal(euRegion.DRtoConfiguration[euRegion.RX2DefaultReceiveWindows.dr].configuration, actual.Txpk.Datr);

            // Ensure tmst was computed to 2 seconds (2 windows in Europe)
            Assert.Equal(2000000, actual.Txpk.Tmst);

            // Get the device from registry
            var deviceDictionary = loRaDeviceRegistry.InternalGetCachedDevicesForDevAddr(devAddr);

            Assert.True(deviceDictionary.TryGetValue(simulatedDevice.DevEUI, out var loRaDevice));
            var payloadDataDown = new LoRaPayloadData(Convert.FromBase64String(downlinkMessage.Txpk.Data));

            payloadDataDown.PerformEncryption(loRaDevice.AppSKey);
            Assert.Equal(payloadDataDown.DevAddr.ToArray(), ConversionHelper.StringToByteArray(loRaDevice.DevAddr));
            Assert.False(payloadDataDown.IsConfirmed());
            Assert.Equal(LoRaMessageType.UnconfirmedDataDown, payloadDataDown.LoRaMessageType);

            // 4. Frame counter up was updated
            Assert.Equal(PayloadFcnt, loRaDevice.FCntUp);

            // 5. Frame counter down is updated
            var expectedFcntDown = InitialDeviceFcntDown + 10 + 1; // adding 10 as buffer when creating a new device instance

            Assert.Equal(expectedFcntDown, loRaDevice.FCntDown);
            Assert.Equal(expectedFcntDown, payloadDataDown.GetFcnt());

            // 6. Frame count has no pending changes
            Assert.False(loRaDevice.HasFrameCountChanges);
        }
        /// <summary>
        /// Process LoRa message where the payload is of type LoRaPayloadData
        /// </summary>
        async Task <DownlinkPktFwdMessage> ProcessDataMessageAsync(LoRaTools.LoRaPhysical.Rxpk rxpk, LoRaPayloadData loraPayload, DateTime startTime)
        {
            var devAddr = loraPayload.DevAddr;

            var timeWatcher = new LoRaOperationTimeWatcher(this.loraRegion, startTime);

            using (var processLogger = new ProcessLogger(timeWatcher, devAddr))
            {
                if (!this.IsValidNetId(loraPayload.GetDevAddrNetID(), this.configuration.NetId))
                {
                    Logger.Log(ConversionHelper.ByteArrayToString(devAddr), "device is using another network id, ignoring this message", LogLevel.Debug);
                    processLogger.LogLevel = LogLevel.Debug;
                    return(null);
                }

                // Find device that matches:
                // - devAddr
                // - mic check (requires: loraDeviceInfo.NwkSKey or loraDeviceInfo.AppKey, rxpk.LoraPayload.Mic)
                // - gateway id
                var loRaDevice = await this.deviceRegistry.GetDeviceForPayloadAsync(loraPayload);

                if (loRaDevice == null)
                {
                    Logger.Log(ConversionHelper.ByteArrayToString(devAddr), $"device is not from our network, ignoring message", LogLevel.Information);
                    return(null);
                }

                // Add context to logger
                processLogger.SetDevEUI(loRaDevice.DevEUI);

                var isMultiGateway       = !string.Equals(loRaDevice.GatewayID, this.configuration.GatewayID, StringComparison.InvariantCultureIgnoreCase);
                var frameCounterStrategy = isMultiGateway ? this.frameCounterUpdateStrategyFactory.GetMultiGatewayStrategy() : this.frameCounterUpdateStrategyFactory.GetSingleGatewayStrategy();

                var payloadFcnt          = loraPayload.GetFcnt();
                var requiresConfirmation = loraPayload.IsConfirmed();

                using (new LoRaDeviceFrameCounterSession(loRaDevice, frameCounterStrategy))
                {
                    // Leaf devices that restart lose the counter. In relax mode we accept the incoming frame counter
                    // ABP device does not reset the Fcnt so in relax mode we should reset for 0 (LMIC based) or 1
                    var isFrameCounterFromNewlyStartedDevice = false;
                    if (payloadFcnt <= 1)
                    {
                        if (loRaDevice.IsABP)
                        {
                            if (loRaDevice.IsABPRelaxedFrameCounter && loRaDevice.FCntUp >= 0 && payloadFcnt <= 1)
                            {
                                // known problem when device restarts, starts fcnt from zero
                                _ = frameCounterStrategy.ResetAsync(loRaDevice);
                                isFrameCounterFromNewlyStartedDevice = true;
                            }
                        }
                        else if (loRaDevice.FCntUp == payloadFcnt && payloadFcnt == 0)
                        {
                            // Some devices start with frame count 0
                            isFrameCounterFromNewlyStartedDevice = true;
                        }
                    }

                    // Reply attack or confirmed reply
                    // Confirmed resubmit: A confirmed message that was received previously but we did not answer in time
                    // Device will send it again and we just need to return an ack (but also check for C2D to send it over)
                    var isConfirmedResubmit = false;
                    if (!isFrameCounterFromNewlyStartedDevice && payloadFcnt <= loRaDevice.FCntUp)
                    {
                        // if it is confirmed most probably we did not ack in time before or device lost the ack packet so we should continue but not send the msg to iothub
                        if (requiresConfirmation && payloadFcnt == loRaDevice.FCntUp)
                        {
                            if (!loRaDevice.ValidateConfirmResubmit(payloadFcnt))
                            {
                                Logger.Log(loRaDevice.DevEUI, $"resubmit from confirmed message exceeds threshold of {LoRaDevice.MaxConfirmationResubmitCount}, message ignored, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Debug);
                                processLogger.LogLevel = LogLevel.Debug;
                                return(null);
                            }

                            isConfirmedResubmit = true;
                            Logger.Log(loRaDevice.DevEUI, $"resubmit from confirmed message detected, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Information);
                        }
                        else
                        {
                            Logger.Log(loRaDevice.DevEUI, $"invalid frame counter, message ignored, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Information);
                            return(null);
                        }
                    }

                    var fcntDown = 0;
                    // If it is confirmed it require us to update the frame counter down
                    // Multiple gateways: in redis, otherwise in device twin
                    if (requiresConfirmation)
                    {
                        fcntDown = await frameCounterStrategy.NextFcntDown(loRaDevice, payloadFcnt);

                        // Failed to update the fcnt down
                        // In multi gateway scenarios it means the another gateway was faster than using, can stop now
                        if (fcntDown <= 0)
                        {
                            // update our fcntup anyway?
                            // loRaDevice.SetFcntUp(payloadFcnt);
                            Logger.Log(loRaDevice.DevEUI, "another gateway has already sent ack or downlink msg", LogLevel.Information);

                            return(null);
                        }

                        Logger.Log(loRaDevice.DevEUI, $"down frame counter: {loRaDevice.FCntDown}", LogLevel.Information);
                    }

                    if (!isConfirmedResubmit)
                    {
                        var validFcntUp = isFrameCounterFromNewlyStartedDevice || (payloadFcnt > loRaDevice.FCntUp);
                        if (validFcntUp)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"valid frame counter, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Information);

                            object payloadData = null;

                            // if it is an upward acknowledgement from the device it does not have a payload
                            // This is confirmation from leaf device that he received a C2D confirmed
                            // if a message payload is null we don't try to decrypt it.
                            if (loraPayload.Frmpayload.Length != 0)
                            {
                                byte[] decryptedPayloadData = null;
                                try
                                {
                                    decryptedPayloadData = loraPayload.GetDecryptedPayload(loRaDevice.AppSKey);
                                }
                                catch (Exception ex)
                                {
                                    Logger.Log(loRaDevice.DevEUI, $"failed to decrypt message: {ex.Message}", LogLevel.Error);
                                }

                                var fportUp = loraPayload.GetFPort();

                                if (string.IsNullOrEmpty(loRaDevice.SensorDecoder))
                                {
                                    Logger.Log(loRaDevice.DevEUI, $"no decoder set in device twin. port: {fportUp}", LogLevel.Debug);
                                    payloadData = Convert.ToBase64String(decryptedPayloadData);
                                }
                                else
                                {
                                    Logger.Log(loRaDevice.DevEUI, $"decoding with: {loRaDevice.SensorDecoder} port: {fportUp}", LogLevel.Debug);
                                    payloadData = await this.payloadDecoder.DecodeMessageAsync(decryptedPayloadData, fportUp, loRaDevice.SensorDecoder);
                                }
                            }

                            if (!await this.SendDeviceEventAsync(loRaDevice, rxpk, payloadData, loraPayload, timeWatcher))
                            {
                                // failed to send event to IoT Hub, stop now
                                return(null);
                            }

                            loRaDevice.SetFcntUp(payloadFcnt);
                        }
                        else
                        {
                            Logger.Log(loRaDevice.DevEUI, $"invalid frame counter, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Information);
                        }
                    }

                    // We check if we have time to futher progress or not
                    // C2D checks are quite expensive so if we are really late we just stop here
                    var timeToSecondWindow = timeWatcher.GetRemainingTimeToReceiveSecondWindow(loRaDevice);
                    if (timeToSecondWindow < LoRaOperationTimeWatcher.ExpectedTimeToPackageAndSendMessage)
                    {
                        if (requiresConfirmation)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"too late for down message ({timeWatcher.GetElapsedTime()}), sending only ACK to gateway", LogLevel.Information);
                        }

                        return(null);
                    }

                    // If it is confirmed and
                    // - Downlink is disabled for the device or
                    // - we don't have time to check c2d and send to device we return now
                    if (requiresConfirmation && (!loRaDevice.DownlinkEnabled || timeToSecondWindow.Subtract(LoRaOperationTimeWatcher.ExpectedTimeToPackageAndSendMessage) <= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage))
                    {
                        return(this.CreateDownlinkMessage(
                                   null,
                                   loRaDevice,
                                   rxpk,
                                   loraPayload,
                                   timeWatcher,
                                   devAddr,
                                   false, // fpending
                                   (ushort)fcntDown));
                    }

                    // Flag indicating if there is another C2D message waiting
                    var fpending = false;

                    // Contains the Cloud to message we need to send
                    Message cloudToDeviceMessage = null;

                    if (loRaDevice.DownlinkEnabled)
                    {
                        // ReceiveAsync has a longer timeout
                        // But we wait less that the timeout (available time before 2nd window)
                        // if message is received after timeout, keep it in loraDeviceInfo and return the next call
                        var timeAvailableToCheckCloudToDeviceMessages = timeWatcher.GetAvailableTimeToCheckCloudToDeviceMessage(loRaDevice);
                        if (timeAvailableToCheckCloudToDeviceMessages >= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage)
                        {
                            cloudToDeviceMessage = await loRaDevice.ReceiveCloudToDeviceAsync(timeAvailableToCheckCloudToDeviceMessages);

                            if (cloudToDeviceMessage != null && !this.ValidateCloudToDeviceMessage(loRaDevice, cloudToDeviceMessage))
                            {
                                _ = loRaDevice.CompleteCloudToDeviceMessageAsync(cloudToDeviceMessage);
                                cloudToDeviceMessage = null;
                            }

                            if (cloudToDeviceMessage != null)
                            {
                                if (!requiresConfirmation)
                                {
                                    // The message coming from the device was not confirmed, therefore we did not computed the frame count down
                                    // Now we need to increment because there is a C2D message to be sent
                                    fcntDown = await frameCounterStrategy.NextFcntDown(loRaDevice, payloadFcnt);

                                    if (fcntDown == 0)
                                    {
                                        // We did not get a valid frame count down, therefore we should not process the message
                                        _ = loRaDevice.AbandonCloudToDeviceMessageAsync(cloudToDeviceMessage);

                                        cloudToDeviceMessage = null;
                                    }
                                    else
                                    {
                                        requiresConfirmation = true;
                                    }

                                    Logger.Log(loRaDevice.DevEUI, $"down frame counter: {loRaDevice.FCntDown}", LogLevel.Information);
                                }

                                // Checking again if cloudToDeviceMessage is valid because the fcntDown resolution could have failed,
                                // causing us to drop the message
                                if (cloudToDeviceMessage != null)
                                {
                                    var remainingTimeForFPendingCheck = timeWatcher.GetRemainingTimeToReceiveSecondWindow(loRaDevice) - (LoRaOperationTimeWatcher.CheckForCloudMessageCallEstimatedOverhead + LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage);
                                    if (remainingTimeForFPendingCheck >= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage)
                                    {
                                        var additionalMsg = await loRaDevice.ReceiveCloudToDeviceAsync(LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage);

                                        if (additionalMsg != null)
                                        {
                                            fpending = true;
                                            _        = loRaDevice.AbandonCloudToDeviceMessageAsync(additionalMsg);
                                            Logger.Log(loRaDevice.DevEUI, $"found fpending c2d message id: {additionalMsg.MessageId ?? "undefined"}", LogLevel.Information);
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // No C2D message and request was not confirmed, return nothing
                    if (!requiresConfirmation)
                    {
                        return(null);
                    }

                    var confirmDownstream = this.CreateDownlinkMessage(
                        cloudToDeviceMessage,
                        loRaDevice,
                        rxpk,
                        loraPayload,
                        timeWatcher,
                        devAddr,
                        fpending,
                        (ushort)fcntDown);

                    if (cloudToDeviceMessage != null)
                    {
                        if (confirmDownstream == null)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"out of time for downstream message, will abandon c2d message id: {cloudToDeviceMessage.MessageId ?? "undefined"}", LogLevel.Information);
                            _ = loRaDevice.AbandonCloudToDeviceMessageAsync(cloudToDeviceMessage);
                        }
                        else
                        {
                            _ = loRaDevice.CompleteCloudToDeviceMessageAsync(cloudToDeviceMessage);
                        }
                    }

                    return(confirmDownstream);
                }
            }
        }