protected override void UpdateState(LoRaADRResult loRaADRResult)
 {
     if (loRaADRResult != null)
     {
         LoRaDevice.UpdatedADRProperties(loRaADRResult.DataRate, loRaADRResult.TxPower.GetValueOrDefault(), loRaADRResult.NbRepetition.GetValueOrDefault());
     }
 }
Beispiel #2
0
        /// <summary>
        /// Prepare the Mac Commands to be sent in the downstream message.
        /// </summary>
        static ICollection <MacCommand> PrepareMacCommandAnswer(
            string devEUI,
            IEnumerable <MacCommand> requestedMacCommands,
            IEnumerable <MacCommand> serverMacCommands,
            Rxpk rxpk,
            LoRaADRResult loRaADRResult)
        {
            var macCommands = new Dictionary <int, MacCommand>();

            if (requestedMacCommands != null)
            {
                foreach (var requestedMacCommand in requestedMacCommands)
                {
                    switch (requestedMacCommand.Cid)
                    {
                    case CidEnum.LinkCheckCmd:
                    {
                        if (rxpk != null)
                        {
                            var linkCheckAnswer = new LinkCheckAnswer(rxpk.GetModulationMargin(), 1);
                            if (macCommands.TryAdd((int)CidEnum.LinkCheckCmd, linkCheckAnswer))
                            {
                                Logger.Log(devEUI, $"answering to a MAC command request {linkCheckAnswer.ToString()}", LogLevel.Information);
                            }
                        }

                        break;
                    }
                    }
                }
            }

            if (serverMacCommands != null)
            {
                foreach (var macCmd in serverMacCommands)
                {
                    if (macCmd != null)
                    {
                        try
                        {
                            if (!macCommands.TryAdd((int)macCmd.Cid, macCmd))
                            {
                                Logger.Log(devEUI, $"could not send the cloud to device MAC command {macCmd.Cid}, as such a property was already present in the message. Please resend the cloud to device message", LogLevel.Error);
                            }

                            Logger.Log(devEUI, $"cloud to device MAC command {macCmd.Cid} received {macCmd}", LogLevel.Information);
                        }
                        catch (MacCommandException ex)
                        {
                            Logger.Log(devEUI, ex.ToString(), LogLevel.Error);
                        }
                    }
                }
            }

            // ADR Part.
            // Currently only replying on ADR Req
            if (loRaADRResult?.CanConfirmToDevice == true)
            {
                const int      placeholderChannel = 25;
                LinkADRRequest linkADR            = new LinkADRRequest((byte)loRaADRResult.DataRate, (byte)loRaADRResult.TxPower, placeholderChannel, 0, (byte)loRaADRResult.NbRepetition);
                macCommands.Add((int)CidEnum.LinkADRCmd, linkADR);
                Logger.Log(devEUI, $"performing a rate adaptation: DR {loRaADRResult.DataRate}, transmit power {loRaADRResult.TxPower}, #repetition {loRaADRResult.NbRepetition}", LogLevel.Information);
            }

            return(macCommands.Values);
        }
Beispiel #3
0
        /// <summary>
        /// Creates downlink message with ack for confirmation or cloud to device message
        /// </summary>
        internal static DownlinkMessageBuilderResponse CreateDownlinkMessage(
            NetworkServerConfiguration configuration,
            LoRaDevice loRaDevice,
            LoRaRequest request,
            LoRaOperationTimeWatcher timeWatcher,
            IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage,
            bool fpending,
            uint fcntDown,
            LoRaADRResult loRaADRResult)
        {
            var fcntDownToSend = ValidateAndConvert16bitFCnt(fcntDown);

            var  upstreamPayload  = (LoRaPayloadData)request.Payload;
            var  rxpk             = request.Rxpk;
            var  loRaRegion       = request.Region;
            bool isMessageTooLong = false;

            // default fport
            byte fctrl = 0;

            if (upstreamPayload.LoRaMessageType == LoRaMessageType.ConfirmedDataUp)
            {
                // Confirm receiving message to device
                fctrl = (byte)FctrlEnum.Ack;
            }

            // Calculate receive window
            var receiveWindow = timeWatcher.ResolveReceiveWindowToUse(loRaDevice);

            if (receiveWindow == Constants.INVALID_RECEIVE_WINDOW)
            {
                // No valid receive window. Abandon the message
                isMessageTooLong = true;
                return(new DownlinkMessageBuilderResponse(null, isMessageTooLong));
            }

            byte[] rndToken = new byte[2];

            lock (RndLock)
            {
                RndDownlinkMessageBuilder.NextBytes(rndToken);
            }

            string datr;
            double freq;
            long   tmst;

            if (receiveWindow == Constants.RECEIVE_WINDOW_2)
            {
                tmst         = rxpk.Tmst + CalculateTime(timeWatcher.GetReceiveWindow2Delay(loRaDevice), loRaDevice.ReportedRXDelay);
                (freq, datr) = loRaRegion.GetDownstreamRX2DRAndFreq(loRaDevice.DevEUI, configuration.Rx2DataRate, configuration.Rx2DataFrequency, loRaDevice.ReportedRX2DataRate);
            }
            else
            {
                datr = loRaRegion.GetDownstreamDR(rxpk, (uint)loRaDevice.ReportedRX1DROffset);
                if (datr == null)
                {
                    Logger.Log(loRaDevice.DevEUI, "there was a problem in setting the data rate in the downstream message packet forwarder settings", LogLevel.Error);
                    return(new DownlinkMessageBuilderResponse(null, false));
                }

                if (!loRaRegion.TryGetUpstreamChannelFrequency(rxpk, out freq))
                {
                    Logger.Log(loRaDevice.DevEUI, "there was a problem in setting the frequency in the downstream message packet forwarder settings", LogLevel.Error);
                    return(new DownlinkMessageBuilderResponse(null, false));
                }

                tmst = rxpk.Tmst + CalculateTime(timeWatcher.GetReceiveWindow1Delay(loRaDevice), loRaDevice.ReportedRXDelay);
            }

            // get max. payload size based on data rate from LoRaRegion
            var maxPayloadSize = loRaRegion.GetMaxPayloadSize(datr);

            // Deduct 8 bytes from max payload size.
            maxPayloadSize -= Constants.LORA_PROTOCOL_OVERHEAD_SIZE;

            var availablePayloadSize = maxPayloadSize;

            var macCommands = new List <MacCommand>();

            byte?fport = null;
            var  requiresDeviceAcknowlegement = false;
            var  macCommandType = CidEnum.Zero;

            byte[] frmPayload = null;

            if (cloudToDeviceMessage != null)
            {
                // Get C2D Mac coomands
                var macCommandsC2d = PrepareMacCommandAnswer(loRaDevice.DevEUI, null, cloudToDeviceMessage?.MacCommands, rxpk, null);

                // Calculate total C2D payload size
                var totalC2dSize = cloudToDeviceMessage.GetPayload()?.Length ?? 0;
                totalC2dSize += macCommandsC2d?.Sum(x => x.Length) ?? 0;

                // Total C2D payload will fit
                if (availablePayloadSize >= totalC2dSize)
                {
                    // Add frmPayload
                    frmPayload = cloudToDeviceMessage.GetPayload();

                    // Add C2D Mac commands
                    if (macCommandsC2d?.Count > 0)
                    {
                        foreach (MacCommand macCommand in macCommandsC2d)
                        {
                            macCommands.Add(macCommand);
                        }
                    }

                    // Deduct frmPayload size from available payload size, continue processing and log
                    availablePayloadSize -= (uint)totalC2dSize;

                    if (cloudToDeviceMessage.Confirmed)
                    {
                        requiresDeviceAcknowlegement         = true;
                        loRaDevice.LastConfirmedC2DMessageID = cloudToDeviceMessage.MessageId ?? Constants.C2D_MSG_ID_PLACEHOLDER;
                    }

                    if (cloudToDeviceMessage.Fport > 0)
                    {
                        fport = cloudToDeviceMessage.Fport;
                    }

                    Logger.Log(loRaDevice.DevEUI, $"cloud to device message: {((frmPayload?.Length ?? 0) == 0 ? "empty" : ConversionHelper.ByteArrayToString(frmPayload))}, id: {cloudToDeviceMessage.MessageId ?? "undefined"}, fport: {fport ?? 0}, confirmed: {requiresDeviceAcknowlegement}, cidType: {macCommandType}, macCommand: {macCommands.Count > 0}", LogLevel.Information);
                    Array.Reverse(frmPayload);
                }
                else
                {
                    // Flag message to be abandoned and log
                    Logger.Log(loRaDevice.DevEUI, $"cloud to device message: {((frmPayload?.Length ?? 0) == 0 ? "empty" : Encoding.UTF8.GetString(frmPayload))}, id: {cloudToDeviceMessage.MessageId ?? "undefined"}, fport: {fport ?? 0}, confirmed: {requiresDeviceAcknowlegement} too long for current receive window. Abandoning.", LogLevel.Debug);
                    isMessageTooLong = true;
                }
            }

            // Get request Mac commands
            var macCommandsRequest = PrepareMacCommandAnswer(loRaDevice.DevEUI, upstreamPayload.MacCommands, null, rxpk, loRaADRResult);

            // Calculate request Mac commands size
            var macCommandsRequestSize = macCommandsRequest?.Sum(x => x.Length) ?? 0;

            // Try adding request Mac commands
            if (availablePayloadSize >= macCommandsRequestSize)
            {
                if (macCommandsRequest?.Count > 0)
                {
                    foreach (MacCommand macCommand in macCommandsRequest)
                    {
                        macCommands.Add(macCommand);
                    }
                }
            }

            if (fpending || isMessageTooLong)
            {
                fctrl |= (int)FctrlEnum.FpendingOrClassB;
            }

            if (upstreamPayload.IsAdrEnabled)
            {
                fctrl |= (byte)FctrlEnum.ADR;
            }

            var srcDevAddr      = upstreamPayload.DevAddr.Span;
            var reversedDevAddr = new byte[srcDevAddr.Length];

            for (int i = reversedDevAddr.Length - 1; i >= 0; --i)
            {
                reversedDevAddr[i] = srcDevAddr[srcDevAddr.Length - (1 + i)];
            }

            var msgType        = requiresDeviceAcknowlegement ? LoRaMessageType.ConfirmedDataDown : LoRaMessageType.UnconfirmedDataDown;
            var ackLoRaMessage = new LoRaPayloadData(
                msgType,
                reversedDevAddr,
                new byte[] { fctrl },
                BitConverter.GetBytes(fcntDownToSend),
                macCommands,
                fport.HasValue ? new byte[] { fport.Value } : null,
                frmPayload,
                1,
                loRaDevice.Supports32BitFCnt ? fcntDown : (uint?)null);

            // todo: check the device twin preference if using confirmed or unconfirmed down
            Logger.Log(loRaDevice.DevEUI, $"sending a downstream message with ID {ConversionHelper.ByteArrayToString(rndToken)}", LogLevel.Information);
            return(new DownlinkMessageBuilderResponse(
                       ackLoRaMessage.Serialize(loRaDevice.AppSKey, loRaDevice.NwkSKey, datr, freq, tmst, loRaDevice.DevEUI),
                       isMessageTooLong));
        }
Beispiel #4
0
        /// <summary>
        /// Prepare the Mac Commands to be sent in the downstream message.
        /// </summary>
        private static ICollection <MacCommand> PrepareMacCommandAnswer(
            IEnumerable <MacCommand> requestedMacCommands,
            IEnumerable <MacCommand> serverMacCommands,
            LoRaRequest loRaRequest,
            LoRaADRResult loRaADRResult,
            ILogger logger)
        {
            var cids        = new HashSet <Cid>();
            var macCommands = new List <MacCommand>();

            if (requestedMacCommands != null)
            {
                foreach (var requestedMacCommand in requestedMacCommands)
                {
                    switch (requestedMacCommand.Cid)
                    {
                    case Cid.LinkCheckCmd:
                    case Cid.Zero:
                    case Cid.One:
                    case Cid.LinkADRCmd:
                        if (loRaRequest != null)
                        {
                            var linkCheckAnswer = new LinkCheckAnswer(checked ((byte)loRaRequest.Region.GetModulationMargin(loRaRequest.RadioMetadata.DataRate, loRaRequest.RadioMetadata.UpInfo.SignalNoiseRatio)), 1);
                            if (cids.Add(Cid.LinkCheckCmd))
                            {
                                macCommands.Add(linkCheckAnswer);
                                logger.LogInformation($"answering to a MAC command request {linkCheckAnswer}");
                            }
                        }
                        break;

                    case Cid.DutyCycleCmd:
                    case Cid.RXParamCmd:
                    case Cid.DevStatusCmd:
                    case Cid.NewChannelCmd:
                    case Cid.RXTimingCmd:
                    case Cid.TxParamSetupCmd:
                    default:
                        break;
                    }
                }
            }

            if (serverMacCommands != null)
            {
                foreach (var macCmd in serverMacCommands)
                {
                    if (macCmd != null)
                    {
                        try
                        {
                            if (cids.Add(macCmd.Cid))
                            {
                                macCommands.Add(macCmd);
                            }
                            else
                            {
                                logger.LogError($"could not send the cloud to device MAC command {macCmd.Cid}, as such a property was already present in the message. Please resend the cloud to device message");
                            }

                            logger.LogInformation($"cloud to device MAC command {macCmd.Cid} received {macCmd}");
                        }
                        catch (MacCommandException ex) when(ExceptionFilterUtility.True(() => logger.LogError(ex.ToString())))
                        {
                            // continue
                        }
                    }
                }
            }

            // ADR Part.
            // Currently only replying on ADR Req
            if (loRaADRResult?.CanConfirmToDevice == true)
            {
                const int placeholderChannel = 25;
                var       linkADR            = new LinkADRRequest((byte)loRaADRResult.DataRate, (byte)loRaADRResult.TxPower, placeholderChannel, 0, (byte)loRaADRResult.NbRepetition);
                macCommands.Add(linkADR);
                logger.LogInformation($"performing a rate adaptation: DR {loRaADRResult.DataRate}, transmit power {loRaADRResult.TxPower}, #repetition {loRaADRResult.NbRepetition}");
            }

            return(macCommands);
        }
Beispiel #5
0
        /// <summary>
        /// Creates downlink message with ack for confirmation or cloud to device message.
        /// </summary>
        internal static DownlinkMessageBuilderResponse CreateDownlinkMessage(
            NetworkServerConfiguration configuration,
            LoRaDevice loRaDevice,
            LoRaRequest request,
            LoRaOperationTimeWatcher timeWatcher,
            IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage,
            bool fpending,
            uint fcntDown,
            LoRaADRResult loRaADRResult,
            ILogger logger)
        {
            var fcntDownToSend = ValidateAndConvert16bitFCnt(fcntDown);

            var upstreamPayload  = (LoRaPayloadData)request.Payload;
            var radioMetadata    = request.RadioMetadata;
            var loRaRegion       = request.Region;
            var isMessageTooLong = false;

            // default fport
            var fctrl = FrameControlFlags.None;

            if (upstreamPayload.MessageType == MacMessageType.ConfirmedDataUp)
            {
                // Confirm receiving message to device
                fctrl = FrameControlFlags.Ack;
            }

            // Calculate receive window
            var receiveWindow = timeWatcher.ResolveReceiveWindowToUse(loRaDevice);

            if (receiveWindow is null)
            {
                // No valid receive window. Abandon the message
                isMessageTooLong = true;
                return(new DownlinkMessageBuilderResponse(null, isMessageTooLong, receiveWindow));
            }

            var rndToken = new byte[2];

            RndKeysGenerator.GetBytes(rndToken);

            DataRateIndex datr;
            Hertz         freq;

            var deviceJoinInfo = request.Region.LoRaRegion == LoRaRegionType.CN470RP2
                ? new DeviceJoinInfo(loRaDevice.ReportedCN470JoinChannel, loRaDevice.DesiredCN470JoinChannel)
                : null;

            if (loRaRegion is DwellTimeLimitedRegion someRegion)
            {
                someRegion.UseDwellTimeSetting(loRaDevice.ReportedDwellTimeSetting);
            }

            if (receiveWindow is ReceiveWindow2)
            {
                freq = loRaRegion.GetDownstreamRX2Freq(configuration.Rx2Frequency, deviceJoinInfo, logger);
                datr = loRaRegion.GetDownstreamRX2DataRate(configuration.Rx2DataRate, loRaDevice.ReportedRX2DataRate, deviceJoinInfo, logger);
            }
            else
            {
                datr = loRaRegion.GetDownstreamDataRate(radioMetadata.DataRate, loRaDevice.ReportedRX1DROffset);

                // The logic for passing CN470 join channel will change as part of #561
                if (!loRaRegion.TryGetDownstreamChannelFrequency(radioMetadata.Frequency, upstreamDataRate: radioMetadata.DataRate, deviceJoinInfo: deviceJoinInfo, downstreamFrequency: out freq))
                {
                    logger.LogError("there was a problem in setting the frequency in the downstream message settings");
                    return(new DownlinkMessageBuilderResponse(null, false, receiveWindow));
                }
            }

            var rx2 = new ReceiveWindow(loRaRegion.GetDownstreamRX2DataRate(configuration.Rx2DataRate, loRaDevice.ReportedRX2DataRate, deviceJoinInfo, logger),
                                        loRaRegion.GetDownstreamRX2Freq(configuration.Rx2Frequency, deviceJoinInfo, logger));

            // get max. payload size based on data rate from LoRaRegion
            var maxPayloadSize = loRaRegion.GetMaxPayloadSize(datr);

            // Deduct 8 bytes from max payload size.
            maxPayloadSize -= Constants.LoraProtocolOverheadSize;

            var availablePayloadSize = maxPayloadSize;

            var macCommands = new List <MacCommand>();

            FramePort?fport = null;
            var       requiresDeviceAcknowlegement = false;
            var       macCommandType = Cid.Zero;

            byte[] frmPayload = null;

            if (cloudToDeviceMessage != null)
            {
                // Get C2D Mac coomands
                var macCommandsC2d = PrepareMacCommandAnswer(null, cloudToDeviceMessage.MacCommands, request, null, logger);

                // Calculate total C2D payload size
                var totalC2dSize = cloudToDeviceMessage.GetPayload()?.Length ?? 0;
                totalC2dSize += macCommandsC2d?.Sum(x => x.Length) ?? 0;

                // Total C2D payload will fit
                if (availablePayloadSize >= totalC2dSize)
                {
                    // Add frmPayload
                    frmPayload = cloudToDeviceMessage.GetPayload();

                    // Add C2D Mac commands
                    if (macCommandsC2d?.Count > 0)
                    {
                        foreach (var macCommand in macCommandsC2d)
                        {
                            macCommands.Add(macCommand);
                        }
                    }

                    // Deduct frmPayload size from available payload size, continue processing and log
                    availablePayloadSize -= (uint)totalC2dSize;

                    if (cloudToDeviceMessage.Confirmed)
                    {
                        requiresDeviceAcknowlegement         = true;
                        loRaDevice.LastConfirmedC2DMessageID = cloudToDeviceMessage.MessageId ?? Constants.C2D_MSG_ID_PLACEHOLDER;
                    }

                    if (cloudToDeviceMessage.Fport.IsAppSpecific() || cloudToDeviceMessage.Fport.IsReserved())
                    {
                        fport = cloudToDeviceMessage.Fport;
                    }

                    logger.LogInformation($"cloud to device message: {((frmPayload?.Length ?? 0) == 0 ? "empty" : frmPayload.ToHex())}, id: {cloudToDeviceMessage.MessageId ?? "undefined"}, fport: {(byte)(fport ?? FramePort.MacCommand)}, confirmed: {requiresDeviceAcknowlegement}, cidType: {macCommandType}, macCommand: {macCommands.Count > 0}");
                }
                else
                {
                    // Flag message to be abandoned and log`
                    logger.LogDebug($"cloud to device message: empty, id: {cloudToDeviceMessage.MessageId ?? "undefined"}, fport: 0, confirmed: {requiresDeviceAcknowlegement} too long for current receive window. Abandoning.");
                    isMessageTooLong = true;
                }
            }

            // Get request Mac commands
            var macCommandsRequest = PrepareMacCommandAnswer(upstreamPayload.MacCommands, null, request, loRaADRResult, logger);

            // Calculate request Mac commands size
            var macCommandsRequestSize = macCommandsRequest?.Sum(x => x.Length) ?? 0;

            // Try adding request Mac commands
            if (availablePayloadSize >= macCommandsRequestSize)
            {
                if (macCommandsRequest?.Count > 0)
                {
                    foreach (var macCommand in macCommandsRequest)
                    {
                        macCommands.Add(macCommand);
                    }
                }
            }

            if (fpending || isMessageTooLong)
            {
                fctrl |= FrameControlFlags.DownlinkFramePending;
            }

            if (upstreamPayload.IsDataRateNetworkControlled)
            {
                fctrl |= FrameControlFlags.Adr;
            }

            var msgType        = requiresDeviceAcknowlegement ? MacMessageType.ConfirmedDataDown : MacMessageType.UnconfirmedDataDown;
            var ackLoRaMessage = new LoRaPayloadData(
                msgType,
                upstreamPayload.DevAddr,
                fctrl,
                fcntDownToSend,
                macCommands,
                fport,
                frmPayload,
                1,
                loRaDevice.Supports32BitFCnt ? fcntDown : null);

            // following calculation is making sure that ReportedRXDelay is chosen if not default,
            // todo: check the device twin preference if using confirmed or unconfirmed down
            var downlinkMessage = BuildDownstreamMessage(loRaDevice,
                                                         request.StationEui,
                                                         logger,
                                                         radioMetadata.UpInfo.Xtime,
                                                         receiveWindow is ReceiveWindow2 ? null : new ReceiveWindow(datr, freq),
                                                         rx2,
                                                         loRaDevice.ReportedRXDelay,
                                                         ackLoRaMessage,
                                                         loRaDevice.ClassType,
                                                         radioMetadata.UpInfo.AntennaPreference);

            if (logger.IsEnabled(LogLevel.Debug))
            {
                logger.LogDebug($"{ackLoRaMessage.MessageType} {JsonConvert.SerializeObject(downlinkMessage)}");
            }

            return(new DownlinkMessageBuilderResponse(downlinkMessage, isMessageTooLong, receiveWindow));
        }
        private async Task <LoRaADRResult> PerformADR(LoRaRequest request, LoRaDevice loRaDevice, LoRaPayloadData loraPayload, uint payloadFcnt, LoRaADRResult loRaADRResult, ILoRaDeviceFrameCounterUpdateStrategy frameCounterStrategy)
        {
            var loRaADRManager = this.loRaADRManagerFactory.Create(this.loRaADRStrategyProvider, frameCounterStrategy, loRaDevice);

            var loRaADRTableEntry = new LoRaADRTableEntry()
            {
                DevEUI    = loRaDevice.DevEUI,
                FCnt      = payloadFcnt,
                GatewayId = this.configuration.GatewayID,
                Snr       = request.Rxpk.Lsnr
            };

            // If the ADR req bit is not set we don't perform rate adaptation.
            if (!loraPayload.IsAdrReq)
            {
                _ = loRaADRManager.StoreADREntryAsync(loRaADRTableEntry);
            }
            else
            {
                loRaADRResult = await loRaADRManager.CalculateADRResultAndAddEntryAsync(
                    loRaDevice.DevEUI,
                    this.configuration.GatewayID,
                    payloadFcnt,
                    loRaDevice.FCntDown,
                    (float)request.Rxpk.RequiredSnr,
                    request.Region.GetDRFromFreqAndChan(request.Rxpk.Datr),
                    request.Region.TXPowertoMaxEIRP.Count - 1,
                    request.Region.MaxADRDataRate,
                    loRaADRTableEntry);

                Logger.Log(loRaDevice.DevEUI, $"device sent ADR ack request, computing an answer", LogLevel.Debug);
            }

            return(loRaADRResult);
        }
        public async Task <LoRaDeviceRequestProcessResult> ProcessRequestAsync(LoRaRequest request, LoRaDevice loRaDevice)
        {
            var timeWatcher = request.GetTimeWatcher();

            using (var deviceConnectionActivity = loRaDevice.BeginDeviceClientConnectionActivity())
            {
                if (deviceConnectionActivity == null)
                {
                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.DeviceClientConnectionFailed));
                }

                var loraPayload = (LoRaPayloadData)request.Payload;

                var payloadFcnt = loraPayload.GetFcnt();

                uint payloadFcntAdjusted = LoRaPayload.InferUpper32BitsForClientFcnt(payloadFcnt, loRaDevice.FCntUp);
                Logger.Log(loRaDevice.DevEUI, $"converted 16bit FCnt {payloadFcnt} to 32bit FCnt {payloadFcntAdjusted}", LogLevel.Debug);

                var payloadPort          = loraPayload.GetFPort();
                var requiresConfirmation = loraPayload.IsConfirmed || loraPayload.IsMacAnswerRequired;

                LoRaADRResult loRaADRResult = null;

                var frameCounterStrategy = this.frameCounterUpdateStrategyProvider.GetStrategy(loRaDevice.GatewayID);
                if (frameCounterStrategy == null)
                {
                    Logger.Log(loRaDevice.DevEUI, $"failed to resolve frame count update strategy, device gateway: {loRaDevice.GatewayID}, message ignored", LogLevel.Error);
                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.ApplicationError));
                }

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

                // 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
                bool isFrameCounterFromNewlyStartedDevice = await this.DetermineIfFramecounterIsFromNewlyStartedDeviceAsync(loRaDevice, payloadFcntAdjusted, frameCounterStrategy);

                // 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)
                if (!ValidateRequest(request, isFrameCounterFromNewlyStartedDevice, payloadFcntAdjusted, loRaDevice, requiresConfirmation, out bool isConfirmedResubmit, out LoRaDeviceRequestProcessResult result))
                {
                    return(result);
                }

                var useMultipleGateways = string.IsNullOrEmpty(loRaDevice.GatewayID);

                try
                {
                    var bundlerResult = await this.TryUseBundler(request, loRaDevice, loraPayload, useMultipleGateways);

                    loRaADRResult = bundlerResult?.AdrResult;

                    if (bundlerResult?.PreferredGatewayResult != null)
                    {
                        this.HandlePreferredGatewayChanges(request, loRaDevice, bundlerResult);
                    }

                    if (loraPayload.IsAdrReq)
                    {
                        Logger.Log(loRaDevice.DevEUI, $"ADR ack request received", LogLevel.Debug);
                    }

                    // ADR should be performed before the deduplication
                    // as we still want to collect the signal info, even if we drop
                    // it in the next step
                    if (loRaADRResult == null && loraPayload.IsAdrEnabled)
                    {
                        loRaADRResult = await this.PerformADR(request, loRaDevice, loraPayload, payloadFcntAdjusted, loRaADRResult, frameCounterStrategy);
                    }

                    if (loRaADRResult?.CanConfirmToDevice == true || loraPayload.IsAdrReq)
                    {
                        // if we got an ADR result or request, we have to send the update to the device
                        requiresConfirmation = true;
                    }

                    if (useMultipleGateways)
                    {
                        // applying the correct deduplication
                        if (bundlerResult?.DeduplicationResult != null && !bundlerResult.DeduplicationResult.CanProcess)
                        {
                            // duplication strategy is indicating that we do not need to continue processing this message
                            Logger.Log(loRaDevice.DevEUI, $"duplication strategy indicated to not process message: {payloadFcnt}", LogLevel.Debug);
                            return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.DeduplicationDrop));
                        }
                    }
                    else
                    {
                        // we must save class C devices regions in order to send c2d messages
                        if (loRaDevice.ClassType == LoRaDeviceClassType.C && request.Region.LoRaRegion != loRaDevice.LoRaRegion)
                        {
                            loRaDevice.UpdateRegion(request.Region.LoRaRegion, acceptChanges: false);
                        }
                    }

                    // if deduplication already processed the next framecounter down, use that
                    uint?fcntDown = loRaADRResult?.FCntDown != null ? loRaADRResult.FCntDown : bundlerResult?.NextFCntDown;

                    if (fcntDown.HasValue)
                    {
                        LogFrameCounterDownState(loRaDevice, fcntDown.Value);
                    }

                    // 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 this.EnsureHasFcntDownAsync(loRaDevice, fcntDown, payloadFcntAdjusted, frameCounterStrategy);

                        // 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)
                        {
                            return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.HandledByAnotherGateway));
                        }
                    }

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

                        object payloadData          = null;
                        byte[] decryptedPayloadData = null;

                        if (loraPayload.Frmpayload.Length > 0)
                        {
                            try
                            {
                                decryptedPayloadData = loraPayload.GetDecryptedPayload(loRaDevice.AppSKey);
                            }
                            catch (Exception ex)
                            {
                                Logger.Log(loRaDevice.DevEUI, $"failed to decrypt message: {ex.Message}", LogLevel.Error);
                            }
                        }

                        if (payloadPort == LoRaFPort.MacCommand)
                        {
                            if (decryptedPayloadData?.Length > 0)
                            {
                                loraPayload.MacCommands = MacCommand.CreateMacCommandFromBytes(loRaDevice.DevEUI, decryptedPayloadData);
                            }

                            if (loraPayload.IsMacAnswerRequired)
                            {
                                fcntDown = await this.EnsureHasFcntDownAsync(loRaDevice, fcntDown, payloadFcntAdjusted, frameCounterStrategy);

                                if (!fcntDown.HasValue || fcntDown <= 0)
                                {
                                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.HandledByAnotherGateway));
                                }

                                requiresConfirmation = true;
                            }
                        }
                        else
                        {
                            if (string.IsNullOrEmpty(loRaDevice.SensorDecoder))
                            {
                                Logger.Log(loRaDevice.DevEUI, $"no decoder set in device twin. port: {payloadPort}", LogLevel.Debug);
                                payloadData = new UndecodedPayload(decryptedPayloadData);
                            }
                            else
                            {
                                Logger.Log(loRaDevice.DevEUI, $"decoding with: {loRaDevice.SensorDecoder} port: {payloadPort}", LogLevel.Debug);
                                var decodePayloadResult = await this.payloadDecoder.DecodeMessageAsync(loRaDevice.DevEUI, decryptedPayloadData, payloadPort, loRaDevice.SensorDecoder);

                                payloadData = decodePayloadResult.GetDecodedPayload();

                                if (decodePayloadResult.CloudToDeviceMessage != null)
                                {
                                    if (string.IsNullOrEmpty(decodePayloadResult.CloudToDeviceMessage.DevEUI) || string.Equals(loRaDevice.DevEUI, decodePayloadResult.CloudToDeviceMessage.DevEUI, StringComparison.InvariantCultureIgnoreCase))
                                    {
                                        // sending c2d to same device
                                        cloudToDeviceMessage = decodePayloadResult.CloudToDeviceMessage;
                                        fcntDown             = await this.EnsureHasFcntDownAsync(loRaDevice, fcntDown, payloadFcntAdjusted, frameCounterStrategy);

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

                                            cloudToDeviceMessage = null;
                                        }
                                        else
                                        {
                                            requiresConfirmation = true;
                                        }
                                    }
                                    else
                                    {
                                        this.SendClassCDeviceMessage(decodePayloadResult.CloudToDeviceMessage);
                                    }
                                }
                            }
                        }

                        if (!isConfirmedResubmit)
                        {
                            // In case it is a Mac Command only we don't want to send it to the IoT Hub
                            if (payloadPort != LoRaFPort.MacCommand)
                            {
                                if (!await this.SendDeviceEventAsync(request, loRaDevice, timeWatcher, payloadData, bundlerResult?.DeduplicationResult, decryptedPayloadData))
                                {
                                    // failed to send event to IoT Hub, stop now
                                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.IoTHubProblem));
                                }
                            }

                            loRaDevice.SetFcntUp(payloadFcntAdjusted);
                        }
                    }

                    // 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()})", LogLevel.Information);
                        }

                        return(new LoRaDeviceRequestProcessResult(loRaDevice, request));
                    }

                    // 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))
                    {
                        var downlinkMessageBuilderResp = DownlinkMessageBuilder.CreateDownlinkMessage(
                            this.configuration,
                            loRaDevice,
                            request,
                            timeWatcher,
                            cloudToDeviceMessage,
                            false, // fpending
                            fcntDown.GetValueOrDefault(),
                            loRaADRResult);

                        if (downlinkMessageBuilderResp.DownlinkPktFwdMessage != null)
                        {
                            _ = request.PacketForwarder.SendDownstreamAsync(downlinkMessageBuilderResp.DownlinkPktFwdMessage);

                            if (cloudToDeviceMessage != null)
                            {
                                if (downlinkMessageBuilderResp.IsMessageTooLong)
                                {
                                    await cloudToDeviceMessage.AbandonAsync();
                                }
                                else
                                {
                                    await cloudToDeviceMessage.CompleteAsync();
                                }
                            }
                        }

                        return(new LoRaDeviceRequestProcessResult(loRaDevice, request, downlinkMessageBuilderResp.DownlinkPktFwdMessage));
                    }

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

                    // If downlink is enabled and we did not get a cloud to device message from decoder
                    // try to get one from IoT Hub C2D
                    if (loRaDevice.DownlinkEnabled && cloudToDeviceMessage == null)
                    {
                        // 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 this.ReceiveCloudToDeviceAsync(loRaDevice, timeAvailableToCheckCloudToDeviceMessages);

                            if (cloudToDeviceMessage != null && !this.ValidateCloudToDeviceMessage(loRaDevice, request, cloudToDeviceMessage))
                            {
                                // Reject cloud to device message based on result from ValidateCloudToDeviceMessage
                                _ = cloudToDeviceMessage.RejectAsync();
                                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 this.EnsureHasFcntDownAsync(loRaDevice, fcntDown, payloadFcntAdjusted, frameCounterStrategy);

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

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

                                // 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 this.ReceiveCloudToDeviceAsync(loRaDevice, LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage);

                                        if (additionalMsg != null)
                                        {
                                            fpending = true;
                                            Logger.Log(loRaDevice.DevEUI, $"found cloud to device message, setting fpending flag, message id: {additionalMsg.MessageId ?? "undefined"}", LogLevel.Information);
                                            _ = additionalMsg.AbandonAsync();
                                        }
                                    }
                                }
                            }
                        }
                    }

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

                    var confirmDownlinkMessageBuilderResp = DownlinkMessageBuilder.CreateDownlinkMessage(
                        this.configuration,
                        loRaDevice,
                        request,
                        timeWatcher,
                        cloudToDeviceMessage,
                        fpending,
                        fcntDown.GetValueOrDefault(),
                        loRaADRResult);

                    if (cloudToDeviceMessage != null)
                    {
                        if (confirmDownlinkMessageBuilderResp.DownlinkPktFwdMessage == null)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"out of time for downstream message, will abandon cloud to device message id: {cloudToDeviceMessage.MessageId ?? "undefined"}", LogLevel.Information);
                            _ = cloudToDeviceMessage.AbandonAsync();
                        }
                        else if (confirmDownlinkMessageBuilderResp.IsMessageTooLong)
                        {
                            Logger.Log(loRaDevice.DevEUI, $"payload will not fit in current receive window, will abandon cloud to device message id: {cloudToDeviceMessage.MessageId ?? "undefined"}", LogLevel.Error);
                            _ = cloudToDeviceMessage.AbandonAsync();
                        }
                        else
                        {
                            _ = cloudToDeviceMessage.CompleteAsync();
                        }
                    }

                    if (confirmDownlinkMessageBuilderResp.DownlinkPktFwdMessage != null)
                    {
                        _ = request.PacketForwarder.SendDownstreamAsync(confirmDownlinkMessageBuilderResp.DownlinkPktFwdMessage);
                    }

                    return(new LoRaDeviceRequestProcessResult(loRaDevice, request, confirmDownlinkMessageBuilderResp.DownlinkPktFwdMessage));
                }
                finally
                {
                    try
                    {
                        await loRaDevice.SaveChangesAsync();
                    }
                    catch (Exception saveChangesException)
                    {
                        Logger.Log(loRaDevice.DevEUI, $"error updating reported properties. {saveChangesException.Message}", LogLevel.Error);
                    }
                }
            }
        }
Beispiel #8
0
        public async System.Threading.Tasks.Task TestADRAsync(string testName, DevEui devEUI, IList <LoRaADRTableEntry> tableEntries, RadioMetadata radioMetadata, bool expectDefaultAnswer, LoRaADRResult expectedResult)
        {
            this.output.WriteLine($"Starting test {testName}");
            var region = TestUtils.TestRegion;
            ILoRaADRStrategyProvider provider = new LoRaADRStrategyProvider(NullLoggerFactory.Instance);

            using var inMemoryStore = new LoRaADRInMemoryStore();
            var loRaADRManager = new Mock <LoRaADRManagerBase>(MockBehavior.Loose, inMemoryStore, provider, NullLogger <LoRaADRManagerBase> .Instance)
            {
                CallBase = true
            };

            _ = loRaADRManager.Setup(x => x.NextFCntDown(It.IsAny <DevEui>(), It.IsAny <string>(), It.IsAny <uint>(), It.IsAny <uint>())).ReturnsAsync(1U);

            // If the test does not expect a default answer we trigger default reset before
            if (!expectDefaultAnswer)
            {
                _ = await loRaADRManager.Object.CalculateADRResultAndAddEntryAsync(devEUI, string.Empty, 1, 1, (float)region.RequiredSnr(radioMetadata.DataRate), radioMetadata.DataRate, region.TXPowertoMaxEIRP.Count - 1, region.MaxADRDataRate, new LoRaADRTableEntry()
                {
                    Snr          = 0,
                    FCnt         = 1,
                    DevEUI       = devEUI,
                    GatewayCount = 1,
                    GatewayId    = "gateway"
                });
            }

            for (var i = 0; i < tableEntries.Count; i++)
            {
                await loRaADRManager.Object.StoreADREntryAsync(tableEntries[i]);
            }

            var adrResult = await loRaADRManager.Object.CalculateADRResultAndAddEntryAsync(devEUI, string.Empty, 1, 1, (float)region.RequiredSnr(radioMetadata.DataRate), radioMetadata.DataRate, region.TXPowertoMaxEIRP.Count - 1, region.MaxADRDataRate);

            Assert.Equal(expectedResult.DataRate, adrResult.DataRate);
            Assert.Equal(expectedResult.NbRepetition, adrResult.NbRepetition);
            Assert.Equal(expectedResult.TxPower, adrResult.TxPower);
            Assert.Equal(expectedResult.FCntDown, adrResult.FCntDown);

            loRaADRManager.Verify(x => x.NextFCntDown(It.IsAny <DevEui>(), It.IsAny <string>(), It.IsAny <uint>(), It.IsAny <uint>()), Times.AtLeastOnce, "NextFCntDown");
            this.output.WriteLine($"Test {testName} finished");
        }
        public ADRTestData()
        {
            // First test not enough entries to send back an answer
            var tableentries = new List <LoRaADRTableEntry>();

            var deviceNameNotEnoughEntries = "notenoughentries";

            for (uint i = 0; i < 10; i++)
            {
                tableentries.Add(new LoRaADRTableEntry()
                {
                    DevEUI       = deviceNameNotEnoughEntries,
                    FCnt         = i,
                    GatewayCount = 1,
                    GatewayId    = "mygateway",
                    Snr          = -20
                });
            }

            Rxpk rxpk = new Rxpk();

            rxpk.Datr = "SF7BW125";
            this.AddRow("Not enough entries to calculate ADR", deviceNameNotEnoughEntries, tableentries, rxpk, true, new LoRaADRResult()
            {
                DataRate     = 5,
                TxPower      = 0,
                NbRepetition = 1,
                FCntDown     = 1
            });

            // **************************************************************
            // Second test enough entries, as very low SNR and max txpower
            // **************************************************************
            var lowerDRTable      = new List <LoRaADRTableEntry>();
            var lowerDRDeviceName = "decreaseTxPower";

            for (uint i = 0; i < 21; i++)
            {
                lowerDRTable.Add(new LoRaADRTableEntry()
                {
                    DevEUI       = lowerDRDeviceName,
                    FCnt         = i,
                    GatewayCount = 1,
                    GatewayId    = "mygateway",
                    Snr          = -20
                });
            }

            Rxpk notenoughentriesrxpk = new Rxpk();

            // Set Input DR to 5
            notenoughentriesrxpk.Datr = "SF7BW125";
            LoRaADRResult loRaADRResult = new LoRaADRResult()
            {
                DataRate     = 0,
                NbRepetition = 1,
                TxPower      = 0
            };

            this.AddRow("ADR setting DR to 0", lowerDRDeviceName, lowerDRTable, notenoughentriesrxpk, false, loRaADRResult);
            // **************************************************************
            // Third test enough entries increase nbrep, as one message every three is received
            // **************************************************************
            var increaseNbReptableentries = new List <LoRaADRTableEntry>();
            var increaseNbRepDeviceName   = "Increase NpRep";

            for (uint i = 0; i < 21; i++)
            {
                increaseNbReptableentries.Add(new LoRaADRTableEntry()
                {
                    DevEUI       = increaseNbRepDeviceName,
                    FCnt         = 3 * i,
                    GatewayCount = 1,
                    GatewayId    = "mygateway",
                    Snr          = -20
                });
            }

            Rxpk increaseNbReprxpk = new Rxpk();

            // DR5
            increaseNbReprxpk.Datr = "SF7BW125";
            LoRaADRResult increaseNbReploRaADRResult = new LoRaADRResult()
            {
                DataRate     = 5,
                NbRepetition = 3,
                TxPower      = 0,
                FCntDown     = 1
            };

            this.AddRow("ADR increase NbRep", increaseNbRepDeviceName, increaseNbReptableentries, increaseNbReprxpk, false, increaseNbReploRaADRResult);

            // **************************************************************
            // Fourth test enough entries decrease nbrep messages pass through
            // ***
            var decreaseNbReptableentries = new List <LoRaADRTableEntry>();
            var decreaseNbRepDeviceName   = "Decrease NpRep";

            // start by setting a high number of nbrep
            for (uint i = 0; i < 21; i++)
            {
                decreaseNbReptableentries.Add(new LoRaADRTableEntry()
                {
                    DevEUI       = decreaseNbRepDeviceName,
                    FCnt         = 3 * i,
                    GatewayCount = 1,
                    GatewayId    = "mygateway",
                    Snr          = 0
                });
            }

            for (uint i = 61; i < 81; i++)
            {
                decreaseNbReptableentries.Add(
                    new LoRaADRTableEntry()
                {
                    DevEUI       = decreaseNbRepDeviceName,
                    FCnt         = i,
                    GatewayCount = 1,
                    GatewayId    = "mygateway",
                    Snr          = 0
                });
            }

            Rxpk decreaseNbReprxpk = new Rxpk();

            // DR5
            decreaseNbReprxpk.Datr = "SF7BW125";
            LoRaADRResult decreaseNbReploRaADRResult = new LoRaADRResult()
            {
                DataRate     = 5,
                NbRepetition = 1,
                TxPower      = 2,
                FCntDown     = 1
            };

            this.AddRow("ADR decrease NbRep", decreaseNbRepDeviceName, decreaseNbReptableentries, decreaseNbReprxpk, false, decreaseNbReploRaADRResult);
        }
 public virtual DownlinkMessageBuilderResponse DownlinkMessageBuilderResponseAssert(LoRaRequest request,
                                                                                    LoRaDevice loRaDevice,
                                                                                    LoRaOperationTimeWatcher timeWatcher,
                                                                                    LoRaADRResult loRaADRResult,
                                                                                    IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage,
                                                                                    uint?fcntDown,
                                                                                    bool fpending)
 {
     ActualCloudToDeviceMessage = cloudToDeviceMessage;
     return(DownlinkMessageBuilder.CreateDownlinkMessage(this.configuration, loRaDevice, request, timeWatcher,
                                                         cloudToDeviceMessage, fpending, fcntDown ?? 0, loRaADRResult, NullLogger.Instance));
 }
 protected override DownlinkMessageBuilderResponse DownlinkMessageBuilderResponse(LoRaRequest request,
                                                                                  LoRaDevice loRaDevice,
                                                                                  LoRaOperationTimeWatcher timeWatcher,
                                                                                  LoRaADRResult loRaADRResult,
                                                                                  IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage,
                                                                                  uint?fcntDown,
                                                                                  bool fpending) =>
 DownlinkMessageBuilderResponseAssert(request, loRaDevice, timeWatcher, loRaADRResult, cloudToDeviceMessage, fcntDown, fpending);
 protected override Task <LoRaADRResult> PerformADR(LoRaRequest request, LoRaDevice loRaDevice, LoRaPayloadData loraPayload, uint payloadFcnt, LoRaADRResult loRaADRResult, ILoRaDeviceFrameCounterUpdateStrategy frameCounterStrategy)
 => Task.FromResult(PerformADRAssert());