public ConcentratorDeduplicationResult CheckDuplicateData(LoRaRequest loRaRequest, LoRaDevice loRaDevice)
        {
            _ = loRaRequest ?? throw new ArgumentNullException(nameof(loRaRequest));
            _ = loRaDevice ?? throw new ArgumentNullException(nameof(loRaDevice));

            var key = CreateCacheKey((LoRaPayloadData)loRaRequest.Payload, loRaDevice);

            if (EnsureFirstMessageInCache(key, loRaRequest, out var previousStation))
            {
                return(ConcentratorDeduplicationResult.NotDuplicate);
            }

            var result = ConcentratorDeduplicationResult.SoftDuplicateDueToDeduplicationStrategy;

            if (previousStation == loRaRequest.StationEui)
            {
                result = ConcentratorDeduplicationResult.DuplicateDueToResubmission;
            }
            else if (loRaDevice.Deduplication == DeduplicationMode.Drop)
            {
                result = ConcentratorDeduplicationResult.Duplicate;
            }

            this.logger.LogDebug($"Data message received from station {loRaRequest.StationEui}. Marked as {result} {Constants.MessageAlreadyEncountered}.");
            return(result);
        }
        void HandlePreferredGatewayChanges(
            LoRaRequest request,
            LoRaDevice loRaDevice,
            FunctionBundlerResult bundlerResult)
        {
            var preferredGatewayResult = bundlerResult.PreferredGatewayResult;

            if (preferredGatewayResult.IsSuccessful())
            {
                var currentIsPreferredGateway = bundlerResult.PreferredGatewayResult.PreferredGatewayID == this.configuration.GatewayID;

                var preferredGatewayChanged = bundlerResult.PreferredGatewayResult.PreferredGatewayID != loRaDevice.PreferredGatewayID;
                if (preferredGatewayChanged)
                {
                    Logger.Log(loRaDevice.DevEUI, $"preferred gateway changed from '{loRaDevice.PreferredGatewayID}' to '{preferredGatewayResult.PreferredGatewayID}'", LogLevel.Debug);
                }

                if (preferredGatewayChanged)
                {
                    loRaDevice.UpdatePreferredGatewayID(bundlerResult.PreferredGatewayResult.PreferredGatewayID, acceptChanges: !currentIsPreferredGateway);
                }

                // Save the region if we are the winning gateway and it changed
                if (request.Region.LoRaRegion != loRaDevice.LoRaRegion)
                {
                    loRaDevice.UpdateRegion(request.Region.LoRaRegion, acceptChanges: !currentIsPreferredGateway);
                }
            }
            else
            {
                Logger.Log(loRaDevice.DevEUI, $"failed to resolve preferred gateway: {preferredGatewayResult}", LogLevel.Error);
            }
        }
        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 ILoRaDeviceRequestQueue GetLoRaRequestQueue(LoRaRequest request)
        {
            if (request is null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            var devAddr = request.Payload.DevAddr;

            if (this.cache.TryGetValue <DeviceLoaderSynchronizer>(GetDevLoaderCacheKey(devAddr), out var deviceLoader))
            {
                return(deviceLoader);
            }

            if (this.deviceCache.TryGetForPayload(request.Payload, out var cachedDevice))
            {
                this.logger.LogDebug("device in cache");
                if (cachedDevice.IsOurDevice)
                {
                    return(cachedDevice);
                }

                return(new ExternalGatewayLoRaRequestQueue(cachedDevice, this.loggerFactory.CreateLogger <ExternalGatewayLoRaRequestQueue>()));
            }

            // not in cache, need to make a single search by dev addr
            return(GetOrCreateLoadingDevicesRequestQueue(devAddr));
        }
Example #5
0
        /// <summary>
        /// Dispatches a request.
        /// </summary>
        public void DispatchRequest(LoRaRequest request)
        {
            if (request is null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            if (request.Payload is null)
            {
                throw new LoRaProcessingException(nameof(request.Payload), LoRaProcessingErrorCode.PayloadNotSet);
            }

            if (request.Region is null)
            {
                throw new LoRaProcessingException(nameof(request.Region), LoRaProcessingErrorCode.RegionNotSet);
            }

            var loggingRequest = new LoggingLoRaRequest(request, this.loggerFactory.CreateLogger <LoggingLoRaRequest>(), this.d2cMessageDeliveryLatencyHistogram);

            if (request.Payload.MessageType == MacMessageType.JoinRequest)
            {
                DispatchLoRaJoinRequest(loggingRequest);
            }
            else if (request.Payload.MessageType is MacMessageType.UnconfirmedDataUp or MacMessageType.ConfirmedDataUp)
            {
                DispatchLoRaDataMessage(loggingRequest);
            }
Example #6
0
        public FunctionBundler CreateIfRequired(
            string gatewayId,
            LoRaPayloadData loRaPayload,
            LoRaDevice loRaDevice,
            IDeduplicationStrategyFactory deduplicationFactory,
            LoRaRequest request)
        {
            if (!string.IsNullOrEmpty(loRaDevice.GatewayID))
            {
                // single gateway mode
                return(null);
            }

            var context = new FunctionBundlerExecutionContext
            {
                DeduplicationFactory = deduplicationFactory,
                FCntDown             = loRaDevice.FCntDown,
                FCntUp      = loRaPayload.GetFcnt(),
                GatewayId   = gatewayId,
                LoRaDevice  = loRaDevice,
                LoRaPayload = loRaPayload,
                Request     = request
            };

            var qualifyingExecutionItems = new List <IFunctionBundlerExecutionItem>(functionItems.Count);

            for (var i = 0; i < functionItems.Count; i++)
            {
                var itm = functionItems[i];
                if (itm.RequiresExecution(context))
                {
                    qualifyingExecutionItems.Add(itm);
                }
            }

            if (qualifyingExecutionItems.Count == 0)
            {
                return(null);
            }

            var bundlerRequest = new FunctionBundlerRequest
            {
                ClientFCntDown = context.FCntDown,
                ClientFCntUp   = context.FCntUp,
                GatewayId      = gatewayId,
                Rssi           = context.Request.Rxpk.Rssi,
            };

            for (var i = 0; i < qualifyingExecutionItems.Count; i++)
            {
                qualifyingExecutionItems[i].Prepare(context, bundlerRequest);
            }

            Logger.Log(loRaDevice.DevEUI, "FunctionBundler request: ", bundlerRequest, LogLevel.Debug);

            return(new FunctionBundler(loRaDevice.DevEUI, this.deviceApi, bundlerRequest, qualifyingExecutionItems, context));
        }
        private bool ValidateCloudToDeviceMessage(LoRaDevice loRaDevice, LoRaRequest request, IReceivedLoRaCloudToDeviceMessage cloudToDeviceMsg)
        {
            if (!cloudToDeviceMsg.IsValid(out var errorMessage))
            {
                Logger.Log(loRaDevice.DevEUI, errorMessage, LogLevel.Error);
                return(false);
            }

            var  rxpk       = request.Rxpk;
            var  loRaRegion = request.Region;
            uint maxPayload;

            // If preferred Window is RX2, this is the max. payload
            if (loRaDevice.PreferredWindow == Constants.RECEIVE_WINDOW_2)
            {
                // Get max. payload size for RX2, considering possilbe user provided Rx2DataRate
                if (string.IsNullOrEmpty(this.configuration.Rx2DataRate))
                {
                    maxPayload = loRaRegion.DRtoConfiguration[loRaRegion.RX2DefaultReceiveWindows.dr].maxPyldSize;
                }
                else
                {
                    maxPayload = loRaRegion.GetMaxPayloadSize(this.configuration.Rx2DataRate);
                }
            }

            // Otherwise, it is RX1.
            else
            {
                maxPayload = loRaRegion.GetMaxPayloadSize(loRaRegion.GetDownstreamDR(rxpk));
            }

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

            // Calculate total C2D message size based on optional C2D Mac commands.
            var totalPayload = cloudToDeviceMsg.GetPayload()?.Length ?? 0;

            if (cloudToDeviceMsg.MacCommands?.Count > 0)
            {
                foreach (MacCommand macCommand in cloudToDeviceMsg.MacCommands)
                {
                    totalPayload += macCommand.Length;
                }
            }

            // C2D message and optional C2D Mac commands are bigger than max. payload size: REJECT.
            // This message can never be delivered.
            if (totalPayload > maxPayload)
            {
                Logger.Log(loRaDevice.DevEUI, $"message payload size ({totalPayload}) exceeds maximum allowed payload size ({maxPayload}) in cloud to device message", LogLevel.Error);
                return(false);
            }

            return(true);
        }
Example #8
0
 private void AddToDeviceQueue(LoRaDevice device, LoRaRequest request)
 {
     if (device.IsOurDevice)
     {
         device.Queue(request);
     }
     else
     {
         Logger.Log(device.DevEUI, $"device is not our device, ignore message", LogLevel.Debug);
         request.NotifyFailed(device, LoRaDeviceRequestFailedReason.BelongsToAnotherGateway);
     }
 }
Example #9
0
 public FunctionBundlerExecutionContext(string gatewayId, uint fCntUp, uint fCntDown,
                                        LoRaPayloadData loRaPayload, LoRaDevice loRaDevice,
                                        IDeduplicationStrategyFactory deduplicationFactory, LoRaRequest request)
 {
     GatewayId            = gatewayId;
     FCntUp               = fCntUp;
     FCntDown             = fCntDown;
     LoRaPayload          = loRaPayload;
     LoRaDevice           = loRaDevice;
     DeduplicationFactory = deduplicationFactory;
     Request              = request;
 }
        void DispatchLoRaDataMessage(LoRaRequest request)
        {
            var loRaPayload = (LoRaPayloadData)request.Payload;

            if (!this.IsValidNetId(loRaPayload))
            {
                Logger.Log(ConversionHelper.ByteArrayToString(loRaPayload.DevAddr), $"device is using another network id, ignoring this message (network: {this.configuration.NetId}, devAddr: {loRaPayload.GetDevAddrNetID()})", LogLevel.Debug);
                request.NotifyFailed(LoRaDeviceRequestFailedReason.InvalidNetId);
                return;
            }

            this.deviceRegistry.GetLoRaRequestQueue(request).Queue(request);
        }
        public ConcentratorDeduplicationResult CheckDuplicateJoin(LoRaRequest loRaRequest)
        {
            _ = loRaRequest ?? throw new ArgumentNullException(nameof(loRaRequest));
            var key = CreateCacheKey((LoRaPayloadJoinRequest)loRaRequest.Payload);

            if (EnsureFirstMessageInCache(key, loRaRequest, out _))
            {
                return(ConcentratorDeduplicationResult.NotDuplicate);
            }

            var result = ConcentratorDeduplicationResult.Duplicate;

            this.logger.LogDebug($"Join received from station {loRaRequest.StationEui}. Marked as {result} {Constants.MessageAlreadyEncountered}.");
            return(result);
        }
Example #12
0
        public void Queue(LoRaRequest request)
        {
            var requestAddedToQueue = false;

            if (this.state != LoaderState.Finished)
            {
                lock (this.queueLock)
                {
                    // add to the queue only if loading is not yet finished
                    if (this.state < LoaderState.DispatchingQueuedItems)
                    {
                        this.queuedRequests.Add(request);
                        requestAddedToQueue = true;
                    }
                }
            }

            if (!requestAddedToQueue)
            {
                var hasDeviceFromAnotherGateway = false;
                foreach (var device in this.existingDevices.Values)
                {
                    if (device.IsOurDevice)
                    {
                        if (device.ValidateMic(request.Payload))
                        {
                            this.AddToDeviceQueue(device, request);
                            return;
                        }
                    }
                    else
                    {
                        hasDeviceFromAnotherGateway = true;
                    }
                }

                // not handled, raised failed event
                var failedReason =
                    hasDeviceFromAnotherGateway ? LoRaDeviceRequestFailedReason.BelongsToAnotherGateway :
                    this.loadingDevicesFailed ? LoRaDeviceRequestFailedReason.ApplicationError :
                    this.existingDevices.Count > 0 ? LoRaDeviceRequestFailedReason.NotMatchingDeviceByMicCheck : LoRaDeviceRequestFailedReason.NotMatchingDeviceByDevAddr;

                this.LogRequestFailed(request, failedReason);

                request.NotifyFailed(failedReason);
            }
        }
        private bool EnsureFirstMessageInCache(object key, LoRaRequest loRaRequest, out StationEui previousStation)
        {
            var stationEui = loRaRequest.StationEui;

            lock (cacheLock)
            {
                if (!this.cache.TryGetValue(key, out previousStation))
                {
                    _ = this.cache.Set(key, stationEui, new MemoryCacheEntryOptions()
                    {
                        SlidingExpiration = DefaultExpiration
                    });
                    return(true);
                }
            }

            return(false);
        }
Example #14
0
        private void LogRequestFailed(LoRaRequest request, LoRaDeviceRequestFailedReason failedReason)
        {
            var deviceId = ConversionHelper.ByteArrayToString(request.Payload.DevAddr);

            switch (failedReason)
            {
            case LoRaDeviceRequestFailedReason.NotMatchingDeviceByMicCheck:
                Logger.Log(deviceId, $"with devAddr {ConversionHelper.ByteArrayToString(request.Payload.DevAddr)} check MIC failed", LogLevel.Debug);
                break;

            case LoRaDeviceRequestFailedReason.NotMatchingDeviceByDevAddr:
                Logger.Log(deviceId, $"device is not our device, ignore message", LogLevel.Debug);
                break;

            case LoRaDeviceRequestFailedReason.ApplicationError:
                Logger.Log(deviceId, "problem resolving device", LogLevel.Error);
                break;
            }
        }
        private static bool ValidateRequest(LoRaRequest request, bool isFrameCounterFromNewlyStartedDevice, uint payloadFcnt, LoRaDevice loRaDevice, bool requiresConfirmation, out bool isConfirmedResubmit, out LoRaDeviceRequestProcessResult result)
        {
            isConfirmedResubmit = false;
            result = null;

            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.Error);
                        result = new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.ConfirmationResubmitThresholdExceeded);
                        return(false);
                    }

                    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.Debug);
                    result = new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.InvalidFrameCounter);
                    return(false);
                }
            }

            // ensuring the framecount difference between the node and the server
            // is <= MAX_FCNT_GAP
            var diff  = payloadFcnt > loRaDevice.FCntUp ? payloadFcnt - loRaDevice.FCntUp : loRaDevice.FCntUp - payloadFcnt;
            var valid = diff <= Constants.MAX_FCNT_GAP;

            if (!valid)
            {
                Logger.Log(loRaDevice.DevEUI, $"invalid frame counter (diverges too much), message ignored, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Error);
                result = new LoRaDeviceRequestProcessResult(loRaDevice, request, LoRaDeviceRequestFailedReason.InvalidFrameCounter);
            }

            return(valid);
        }
        private async Task <bool> SendDeviceEventAsync(LoRaRequest request, LoRaDevice loRaDevice, LoRaOperationTimeWatcher timeWatcher, object decodedValue, DeduplicationResult deduplicationResult, byte[] decryptedPayloadData)
        {
            var loRaPayloadData = (LoRaPayloadData)request.Payload;
            var deviceTelemetry = new LoRaDeviceTelemetry(request.Rxpk, loRaPayloadData, decodedValue, decryptedPayloadData)
            {
                DeviceEUI = loRaDevice.DevEUI,
                GatewayID = this.configuration.GatewayID,
                Edgets    = (long)(timeWatcher.Start - DateTime.UnixEpoch).TotalMilliseconds
            };

            if (deduplicationResult != null && deduplicationResult.IsDuplicate)
            {
                deviceTelemetry.DupMsg = true;
            }

            Dictionary <string, string> eventProperties = null;

            if (loRaPayloadData.IsUpwardAck())
            {
                eventProperties = new Dictionary <string, string>();
                Logger.Log(loRaDevice.DevEUI, $"message ack received for cloud to device message id {loRaDevice.LastConfirmedC2DMessageID}", LogLevel.Information);
                eventProperties.Add(Constants.C2D_MSG_PROPERTY_VALUE_NAME, loRaDevice.LastConfirmedC2DMessageID ?? Constants.C2D_MSG_ID_PLACEHOLDER);
                loRaDevice.LastConfirmedC2DMessageID = null;
            }

            this.ProcessAndSendMacCommands(loRaPayloadData, ref eventProperties);

            if (await loRaDevice.SendEventAsync(deviceTelemetry, eventProperties))
            {
                string payloadAsRaw = null;
                if (deviceTelemetry.Data != null)
                {
                    payloadAsRaw = JsonConvert.SerializeObject(deviceTelemetry.Data, Formatting.None);
                }

                Logger.Log(loRaDevice.DevEUI, $"message '{payloadAsRaw}' sent to hub", LogLevel.Information);
                return(true);
            }

            return(false);
        }
        protected virtual void ProcessRequest(LoRaRequest request)
        {
            if (LoadingDevicesFailed)
            {
                LogAndNotifyFailedRequest(LoRaDeviceRequestFailedReason.ApplicationError);
                return;
            }

            if (!this.loraDeviceCache.HasRegistrations(this.devAddr))
            {
                LogAndNotifyFailedRequest(LoRaDeviceRequestFailedReason.NotMatchingDeviceByDevAddr);
                return;
            }

            if (this.loraDeviceCache.TryGetForPayload(request.Payload, out var device))
            {
                if (device.IsOurDevice)
                {
                    device.Queue(request);
                }
                else
                {
                    LogAndNotifyFailedRequest(LoRaDeviceRequestFailedReason.BelongsToAnotherGateway);
                }
            }
            else if (this.loraDeviceCache.HasRegistrationsForOtherGateways(this.devAddr))
            {
                LogAndNotifyFailedRequest(LoRaDeviceRequestFailedReason.BelongsToAnotherGateway);
            }
            else
            {
                LogAndNotifyFailedRequest(LoRaDeviceRequestFailedReason.NotMatchingDeviceByMicCheck);
            }

            void LogAndNotifyFailedRequest(LoRaDeviceRequestFailedReason reason)
            {
                LogRequestFailed(request, reason);
                request.NotifyFailed(reason);
            }
        }
        /// <summary>
        /// Dispatches a request
        /// </summary>
        public void DispatchRequest(LoRaRequest request)
        {
            if (!LoRaPayload.TryCreateLoRaPayload(request.Rxpk, out LoRaPayload loRaPayload))
            {
                Logger.Log("There was a problem in decoding the Rxpk", LogLevel.Error);
                request.NotifyFailed(LoRaDeviceRequestFailedReason.InvalidRxpk);
                return;
            }

            if (this.loraRegion == null)
            {
                if (!RegionManager.TryResolveRegion(request.Rxpk, out var currentRegion))
                {
                    // log is generated in Region factory
                    // move here once V2 goes GA
                    request.NotifyFailed(LoRaDeviceRequestFailedReason.InvalidRegion);
                    return;
                }

                this.loraRegion = currentRegion;
            }

            request.SetPayload(loRaPayload);
            request.SetRegion(this.loraRegion);

            var loggingRequest = new LoggingLoRaRequest(request);

            if (loRaPayload.LoRaMessageType == LoRaMessageType.JoinRequest)
            {
                this.DispatchLoRaJoinRequest(loggingRequest);
            }
            else if (loRaPayload.LoRaMessageType == LoRaMessageType.UnconfirmedDataUp || loRaPayload.LoRaMessageType == LoRaMessageType.ConfirmedDataUp)
            {
                this.DispatchLoRaDataMessage(loggingRequest);
            }
            else
            {
                Logger.Log("Unknwon message type in rxpk, message ignored", LogLevel.Error);
            }
        }
        public void Queue(LoRaRequest request)
        {
            var requestAddedToQueue = false;

            if (this.state != LoaderState.Finished)
            {
                lock (this.queueLock)
                {
                    // add to the queue only if loading is not yet finished
                    if (this.state < LoaderState.DispatchingQueuedItems)
                    {
                        this.queuedRequests.Add(request);
                        requestAddedToQueue = true;
                    }
                }
            }

            if (!requestAddedToQueue)
            {
                ProcessRequest(request);
            }
        }
        private async Task <FunctionBundlerResult> TryUseBundler(LoRaRequest request, LoRaDevice loRaDevice, LoRaPayloadData loraPayload, bool useMultipleGateways)
        {
            FunctionBundlerResult bundlerResult = null;

            if (useMultipleGateways)
            {
                var bundler = this.functionBundlerProvider.CreateIfRequired(this.configuration.GatewayID, loraPayload, loRaDevice, this.deduplicationFactory, request);
                if (bundler != null)
                {
                    bundlerResult = await bundler.Execute();

                    if (bundlerResult.NextFCntDown.HasValue)
                    {
                        // we got a new framecounter down. Make sure this
                        // gets saved eventually to the twins
                        loRaDevice.SetFcntDown(bundlerResult.NextFCntDown.Value);
                    }
                }
            }

            return(bundlerResult);
        }
Example #21
0
        public ILoRaDeviceRequestQueue GetLoRaRequestQueue(LoRaRequest request)
        {
            var devAddr = ConversionHelper.ByteArrayToString(request.Payload.DevAddr);
            var devicesMatchingDevAddr = this.InternalGetCachedDevicesForDevAddr(devAddr);

            // If already in cache, return quickly
            if (devicesMatchingDevAddr.Count > 0)
            {
                var cachedMatchingDevice = devicesMatchingDevAddr.Values.FirstOrDefault(x => this.IsValidDeviceForPayload(x, (LoRaPayloadData)request.Payload, logError: false));
                if (cachedMatchingDevice != null)
                {
                    Logger.Log(cachedMatchingDevice.DevEUI, "device in cache", LogLevel.Debug);
                    if (cachedMatchingDevice.IsOurDevice)
                    {
                        return(cachedMatchingDevice);
                    }

                    return(new ExternalGatewayLoRaRequestQueue(cachedMatchingDevice));
                }
            }

            // not in cache, need to make a single search by dev addr
            return(this.GetOrCreateLoadingDevicesRequestQueue(devAddr));
        }
        private void LogRequestFailed(LoRaRequest request, LoRaDeviceRequestFailedReason failedReason)
        {
            switch (failedReason)
            {
            case LoRaDeviceRequestFailedReason.NotMatchingDeviceByMicCheck:
                this.logger.LogDebug($"with devAddr {request.Payload.DevAddr} check MIC failed");
                break;

            case LoRaDeviceRequestFailedReason.BelongsToAnotherGateway:
            case LoRaDeviceRequestFailedReason.NotMatchingDeviceByDevAddr:
                this.logger.LogDebug("device is not our device, ignore message");
                break;

            case LoRaDeviceRequestFailedReason.ApplicationError:
                this.logger.LogError("problem resolving device");
                break;

            case LoRaDeviceRequestFailedReason.InvalidNetId:
            case LoRaDeviceRequestFailedReason.InvalidUpstreamMessage:
            case LoRaDeviceRequestFailedReason.InvalidRegion:
            case LoRaDeviceRequestFailedReason.UnknownDevice:
            case LoRaDeviceRequestFailedReason.InvalidJoinRequest:
            case LoRaDeviceRequestFailedReason.HandledByAnotherGateway:
            case LoRaDeviceRequestFailedReason.JoinDevNonceAlreadyUsed:
            case LoRaDeviceRequestFailedReason.JoinMicCheckFailed:
            case LoRaDeviceRequestFailedReason.ReceiveWindowMissed:
            case LoRaDeviceRequestFailedReason.ConfirmationResubmitThresholdExceeded:
            case LoRaDeviceRequestFailedReason.InvalidFrameCounter:
            case LoRaDeviceRequestFailedReason.IoTHubProblem:
            case LoRaDeviceRequestFailedReason.DeduplicationDrop:
            case LoRaDeviceRequestFailedReason.DeviceClientConnectionFailed:
            default:
                this.logger.LogDebug("device request failed");
                break;
            }
        }
Example #23
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));
        }
 public void Queue(LoRaRequest request)
 {
     logger.LogDebug("device is not our device, ignore message");
     request.NotifyFailed(this.loRaDevice, LoRaDeviceRequestFailedReason.BelongsToAnotherGateway);
 }
        internal async Task ProcessJoinRequestAsync(LoRaRequest request)
        {
            LoRaDevice loRaDevice = null;
            var        loraRegion = request.Region;

            try
            {
                var timeWatcher       = request.GetTimeWatcher();
                var processingTimeout = timeWatcher.GetRemainingTimeToJoinAcceptSecondWindow() - TimeSpan.FromMilliseconds(100);
                using var joinAcceptCancellationToken = new CancellationTokenSource(processingTimeout > TimeSpan.Zero ? processingTimeout : TimeSpan.Zero);

                var joinReq = (LoRaPayloadJoinRequest)request.Payload;

                var devEui = joinReq.DevEUI;

                using var scope = this.logger.BeginDeviceScope(devEui);

                this.logger.LogInformation("join request received");

                if (this.concentratorDeduplication.CheckDuplicateJoin(request) is ConcentratorDeduplicationResult.Duplicate)
                {
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.DeduplicationDrop);
                    // we do not log here as the concentratorDeduplication service already does more detailed logging
                    return;
                }

                loRaDevice = await this.deviceRegistry.GetDeviceForJoinRequestAsync(devEui, joinReq.DevNonce);

                if (loRaDevice == null)
                {
                    request.NotifyFailed(devEui.ToString(), LoRaDeviceRequestFailedReason.UnknownDevice);
                    // we do not log here as we assume that the deviceRegistry does a more informed logging if returning null
                    return;
                }

                if (loRaDevice.AppKey is null)
                {
                    this.logger.LogError("join refused: missing AppKey for OTAA device");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidJoinRequest);
                    return;
                }

                var appKey = loRaDevice.AppKey.Value;

                this.joinRequestCounter?.Add(1);

                if (loRaDevice.AppEui != joinReq.AppEui)
                {
                    this.logger.LogError("join refused: AppEUI for OTAA does not match device");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidJoinRequest);
                    return;
                }

                if (!joinReq.CheckMic(appKey))
                {
                    this.logger.LogError("join refused: invalid MIC");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.JoinMicCheckFailed);
                    return;
                }

                // Make sure that is a new request and not a replay
                if (loRaDevice.DevNonce is { } devNonce&& devNonce == joinReq.DevNonce)
                {
                    if (string.IsNullOrEmpty(loRaDevice.GatewayID))
                    {
                        this.logger.LogInformation("join refused: join already processed by another gateway");
                    }
                    else
                    {
                        this.logger.LogError("join refused: DevNonce already used by this device");
                    }

                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.JoinDevNonceAlreadyUsed);
                    return;
                }

                // Check that the device is joining through the linked gateway and not another
                if (!loRaDevice.IsOurDevice)
                {
                    this.logger.LogInformation("join refused: trying to join not through its linked gateway, ignoring join request");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.HandledByAnotherGateway);
                    return;
                }

                var netId    = this.configuration.NetId;
                var appNonce = new AppNonce(RandomNumberGenerator.GetInt32(toExclusive: AppNonce.MaxValue + 1));
                var appSKey  = OTAAKeysGenerator.CalculateAppSessionKey(appNonce, netId, joinReq.DevNonce, appKey);
                var nwkSKey  = OTAAKeysGenerator.CalculateNetworkSessionKey(appNonce, netId, joinReq.DevNonce, appKey);
                var address  = RandomNumberGenerator.GetInt32(toExclusive: DevAddr.MaxNetworkAddress + 1);
                // The 7 LBS of the NetID become the NwkID of a DevAddr:
                var devAddr = new DevAddr(unchecked ((byte)netId.NetworkId), address);

                var oldDevAddr = loRaDevice.DevAddr;

                if (!timeWatcher.InTimeForJoinAccept())
                {
                    this.receiveWindowMisses?.Add(1);
                    // in this case it's too late, we need to break and avoid saving twins
                    this.logger.LogInformation("join refused: processing of the join request took too long, sending no message");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.ReceiveWindowMissed);
                    return;
                }

                var updatedProperties = new LoRaDeviceJoinUpdateProperties
                {
                    DevAddr            = devAddr,
                    NwkSKey            = nwkSKey,
                    AppSKey            = appSKey,
                    AppNonce           = appNonce,
                    DevNonce           = joinReq.DevNonce,
                    NetId              = netId,
                    Region             = request.Region.LoRaRegion,
                    PreferredGatewayID = this.configuration.GatewayID,
                };

                if (loRaDevice.ClassType == LoRaDeviceClassType.C)
                {
                    updatedProperties.SavePreferredGateway = true;
                    updatedProperties.SaveRegion           = true;
                    updatedProperties.StationEui           = request.StationEui;
                }

                DeviceJoinInfo deviceJoinInfo = null;
                if (request.Region.LoRaRegion == LoRaRegionType.CN470RP2)
                {
                    if (request.Region.TryGetJoinChannelIndex(request.RadioMetadata.Frequency, out var channelIndex))
                    {
                        updatedProperties.CN470JoinChannel = channelIndex;
                        deviceJoinInfo = new DeviceJoinInfo(channelIndex);
                    }
                    else
                    {
                        this.logger.LogError("failed to retrieve the join channel index for device");
                    }
                }

                var deviceUpdateSucceeded = await loRaDevice.UpdateAfterJoinAsync(updatedProperties, joinAcceptCancellationToken.Token);

                if (!deviceUpdateSucceeded)
                {
                    this.logger.LogError("join refused: join request could not save twin");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.IoTHubProblem);
                    return;
                }

                var windowToUse = timeWatcher.ResolveJoinAcceptWindowToUse();
                if (windowToUse is null)
                {
                    this.receiveWindowMisses?.Add(1);
                    this.logger.LogInformation("join refused: processing of the join request took too long, sending no message");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.ReceiveWindowMissed);
                    return;
                }

                this.deviceRegistry.UpdateDeviceAfterJoin(loRaDevice, oldDevAddr);

                // Build join accept downlink message

                // Build the DlSettings fields that is a superposition of RX2DR and RX1DROffset field
                var dlSettings = new byte[1];

                if (loRaDevice.DesiredRX2DataRate.HasValue)
                {
                    if (request.Region.DRtoConfiguration.ContainsKey(loRaDevice.DesiredRX2DataRate.Value))
                    {
                        dlSettings[0] = (byte)((byte)loRaDevice.DesiredRX2DataRate & 0b00001111);
                    }
                    else
                    {
                        this.logger.LogError("twin RX2 DR value is not within acceptable values");
                    }
                }

                if (request.Region.IsValidRX1DROffset(loRaDevice.DesiredRX1DROffset))
                {
                    var rx1droffset = (byte)(loRaDevice.DesiredRX1DROffset << 4);
                    dlSettings[0] = (byte)(dlSettings[0] + rx1droffset);
                }
                else
                {
                    this.logger.LogError("twin RX1 offset DR value is not within acceptable values");
                }

                // The following DesiredRxDelay is different than the RxDelay to be passed to Serialize function
                // This one is a delay between TX and RX for any message to be processed by joining device
                // The field accepted by Serialize method is an indication of the delay (compared to receive time of join request)
                // of when the message Join Accept message should be sent
                var loraSpecDesiredRxDelay = RxDelay.RxDelay0;
                if (Enum.IsDefined(loRaDevice.DesiredRXDelay))
                {
                    loraSpecDesiredRxDelay = loRaDevice.DesiredRXDelay;
                }
                else
                {
                    this.logger.LogError("twin RX delay value is not within acceptable values");
                }

                var loRaPayloadJoinAccept = new LoRaPayloadJoinAccept(
                    netId,   // NETID 0 / 1 is default test
                    devAddr, // todo add device address management
                    appNonce,
                    dlSettings,
                    loraSpecDesiredRxDelay,
                    null);

                if (!loraRegion.TryGetDownstreamChannelFrequency(request.RadioMetadata.Frequency, upstreamDataRate: request.RadioMetadata.DataRate, deviceJoinInfo: deviceJoinInfo, downstreamFrequency: out var freq))
                {
                    this.logger.LogError("could not resolve DR and/or frequency for downstream");
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidUpstreamMessage);
                    return;
                }

                var joinAcceptBytes = loRaPayloadJoinAccept.Serialize(appKey);

                var rx1 = windowToUse is not ReceiveWindow2
                        ? new ReceiveWindow(loraRegion.GetDownstreamDataRate(request.RadioMetadata.DataRate, loRaDevice.ReportedRX1DROffset), freq)
                        : (ReceiveWindow?)null;

                var rx2 = new ReceiveWindow(loraRegion.GetDownstreamRX2DataRate(this.configuration.Rx2DataRate, null, deviceJoinInfo, this.logger),
                                            loraRegion.GetDownstreamRX2Freq(this.configuration.Rx2Frequency, deviceJoinInfo, this.logger));

                var downlinkMessage = new DownlinkMessage(joinAcceptBytes,
                                                          request.RadioMetadata.UpInfo.Xtime,
                                                          rx1,
                                                          rx2,
                                                          loRaDevice.DevEUI,
                                                          loraRegion.JoinAcceptDelay1,
                                                          loRaDevice.ClassType,
                                                          request.StationEui,
                                                          request.RadioMetadata.UpInfo.AntennaPreference);

                this.receiveWindowHits?.Add(1, KeyValuePair.Create(MetricRegistry.ReceiveWindowTagName, (object)windowToUse));
                _ = request.DownstreamMessageSender.SendDownstreamAsync(downlinkMessage);
                request.NotifySucceeded(loRaDevice, downlinkMessage);

                if (this.logger.IsEnabled(LogLevel.Debug))
                {
                    var jsonMsg = JsonConvert.SerializeObject(downlinkMessage);
                    this.logger.LogDebug($"{MacMessageType.JoinAccept} {jsonMsg}");
                }
                else
                {
                    this.logger.LogInformation("join accepted");
                }
            }
            catch (Exception ex) when
                (ExceptionFilterUtility.True(() => this.logger.LogError(ex, $"failed to handle join request. {ex.Message}", LogLevel.Error),
                                             () => this.unhandledExceptionCount?.Add(1)))
            {
                request.NotifyFailed(loRaDevice, ex);
                throw;
            }
        }
 public void DispatchRequest(LoRaRequest request)
 {
     // Unobserved task exceptions are logged as part of ProcessJoinRequestAsync.
     _ = Task.Run(() => ProcessJoinRequestAsync(request));
 }
Example #27
0
        /// <summary>
        /// Process OTAA join request
        /// </summary>
        async Task ProcessJoinRequestAsync(LoRaRequest request)
        {
            LoRaDevice loRaDevice = null;
            string     devEUI     = null;
            var        loraRegion = request.Region;

            try
            {
                var timeWatcher = new LoRaOperationTimeWatcher(loraRegion, request.StartTime);

                var    joinReq = (LoRaPayloadJoinRequest)request.Payload;
                byte[] udpMsgForPktForwarder = new byte[0];

                devEUI = joinReq.GetDevEUIAsString();
                var appEUI = joinReq.GetAppEUIAsString();

                var devNonce = joinReq.GetDevNonceAsString();
                Logger.Log(devEUI, $"join request received", LogLevel.Information);

                loRaDevice = await this.deviceRegistry.GetDeviceForJoinRequestAsync(devEUI, appEUI, devNonce);

                if (loRaDevice == null)
                {
                    request.NotifyFailed(devEUI, LoRaDeviceRequestFailedReason.UnknownDevice);
                    // we do not log here as we assume that the deviceRegistry does a more informed logging if returning null
                    return;
                }

                if (string.IsNullOrEmpty(loRaDevice.AppKey))
                {
                    Logger.Log(loRaDevice.DevEUI, "join refused: missing AppKey for OTAA device", LogLevel.Error);
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidJoinRequest);
                    return;
                }

                if (loRaDevice.AppEUI != appEUI)
                {
                    Logger.Log(devEUI, "join refused: AppEUI for OTAA does not match device", LogLevel.Error);
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidJoinRequest);
                    return;
                }

                if (!joinReq.CheckMic(loRaDevice.AppKey))
                {
                    Logger.Log(devEUI, "join refused: invalid MIC", LogLevel.Error);
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.JoinMicCheckFailed);
                    return;
                }

                // Make sure that is a new request and not a replay
                if (!string.IsNullOrEmpty(loRaDevice.DevNonce) && loRaDevice.DevNonce == devNonce)
                {
                    if (string.IsNullOrEmpty(loRaDevice.GatewayID))
                    {
                        Logger.Log(devEUI, "join refused: join already processed by another gateway", LogLevel.Information);
                    }
                    else
                    {
                        Logger.Log(devEUI, "join refused: DevNonce already used by this device", LogLevel.Error);
                    }

                    loRaDevice.IsOurDevice = false;
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.JoinDevNonceAlreadyUsed);
                    return;
                }

                // Check that the device is joining through the linked gateway and not another
                if (!string.IsNullOrEmpty(loRaDevice.GatewayID) && !string.Equals(loRaDevice.GatewayID, this.configuration.GatewayID, StringComparison.InvariantCultureIgnoreCase))
                {
                    Logger.Log(devEUI, $"join refused: trying to join not through its linked gateway, ignoring join request", LogLevel.Information);
                    loRaDevice.IsOurDevice = false;
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.HandledByAnotherGateway);
                    return;
                }

                var netIdBytes = BitConverter.GetBytes(this.configuration.NetId);
                var netId      = new byte[3]
                {
                    netIdBytes[0],
                    netIdBytes[1],
                    netIdBytes[2]
                };

                var appNonce      = OTAAKeysGenerator.GetAppNonce();
                var appNonceBytes = LoRaTools.Utils.ConversionHelper.StringToByteArray(appNonce);
                var appKeyBytes   = LoRaTools.Utils.ConversionHelper.StringToByteArray(loRaDevice.AppKey);
                var appSKey       = OTAAKeysGenerator.CalculateKey(new byte[1] {
                    0x02
                }, appNonceBytes, netId, joinReq.DevNonce, appKeyBytes);
                var nwkSKey = OTAAKeysGenerator.CalculateKey(new byte[1] {
                    0x01
                }, appNonceBytes, netId, joinReq.DevNonce, appKeyBytes);
                var devAddr = OTAAKeysGenerator.GetNwkId(netId);

                var oldDevAddr = loRaDevice.DevAddr;

                if (!timeWatcher.InTimeForJoinAccept())
                {
                    // in this case it's too late, we need to break and avoid saving twins
                    Logger.Log(devEUI, $"join refused: processing of the join request took too long, sending no message", LogLevel.Information);
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.ReceiveWindowMissed);
                    return;
                }

                var updatedProperties = new LoRaDeviceJoinUpdateProperties
                {
                    DevAddr            = devAddr,
                    NwkSKey            = nwkSKey,
                    AppSKey            = appSKey,
                    AppNonce           = appNonce,
                    DevNonce           = devNonce,
                    NetID              = ConversionHelper.ByteArrayToString(netId),
                    Region             = request.Region.LoRaRegion,
                    PreferredGatewayID = this.configuration.GatewayID,
                };

                if (loRaDevice.ClassType == LoRaDeviceClassType.C)
                {
                    updatedProperties.SavePreferredGateway = true;
                    updatedProperties.SaveRegion           = true;
                }

                var deviceUpdateSucceeded = await loRaDevice.UpdateAfterJoinAsync(updatedProperties);

                if (!deviceUpdateSucceeded)
                {
                    Logger.Log(devEUI, $"join refused: join request could not save twin", LogLevel.Error);
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.ApplicationError);
                    return;
                }

                var windowToUse = timeWatcher.ResolveJoinAcceptWindowToUse(loRaDevice);
                if (windowToUse == Constants.INVALID_RECEIVE_WINDOW)
                {
                    Logger.Log(devEUI, $"join refused: processing of the join request took too long, sending no message", LogLevel.Information);
                    request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.ReceiveWindowMissed);
                    return;
                }

                double freq = 0;
                string datr = null;
                uint   tmst = 0;
                if (windowToUse == Constants.RECEIVE_WINDOW_1)
                {
                    datr = loraRegion.GetDownstreamDR(request.Rxpk);
                    if (!loraRegion.TryGetUpstreamChannelFrequency(request.Rxpk, out freq) || datr == null)
                    {
                        Logger.Log(loRaDevice.DevEUI, "could not resolve DR and/or frequency for downstream", LogLevel.Error);
                        request.NotifyFailed(loRaDevice, LoRaDeviceRequestFailedReason.InvalidRxpk);
                        return;
                    }

                    // set tmst for the normal case
                    tmst = request.Rxpk.Tmst + loraRegion.Join_accept_delay1 * 1000000;
                }
                else
                {
                    Logger.Log(devEUI, $"processing of the join request took too long, using second join accept receive window", LogLevel.Debug);
                    tmst = request.Rxpk.Tmst + loraRegion.Join_accept_delay2 * 1000000;

                    (freq, datr) = loraRegion.GetDownstreamRX2DRAndFreq(devEUI, this.configuration.Rx2DataRate, this.configuration.Rx2DataFrequency, null);
                }

                loRaDevice.IsOurDevice = true;
                this.deviceRegistry.UpdateDeviceAfterJoin(loRaDevice, oldDevAddr);

                // Build join accept downlink message
                Array.Reverse(netId);
                Array.Reverse(appNonceBytes);

                // Build the DlSettings fields that is a superposition of RX2DR and RX1DROffset field
                byte[] dlSettings = new byte[1];

                if (request.Region.DRtoConfiguration.ContainsKey(loRaDevice.DesiredRX2DataRate))
                {
                    dlSettings[0] =
                        (byte)(loRaDevice.DesiredRX2DataRate & 0b00001111);
                }
                else
                {
                    Logger.Log(devEUI, $"twin RX2 DR value is not within acceptable values", LogLevel.Error);
                }

                if (request.Region.IsValidRX1DROffset(loRaDevice.DesiredRX1DROffset))
                {
                    var rx1droffset = (byte)(loRaDevice.DesiredRX1DROffset << 4);
                    dlSettings[0] = (byte)(dlSettings[0] + rx1droffset);
                }
                else
                {
                    Logger.Log(devEUI, $"twin RX1 offset DR value is not within acceptable values", LogLevel.Error);
                }

                ushort rxDelay = 0;
                if (request.Region.IsValidRXDelay(loRaDevice.DesiredRXDelay))
                {
                    rxDelay = loRaDevice.DesiredRXDelay;
                }
                else
                {
                    Logger.Log(devEUI, $"twin RX delay value is not within acceptable values", LogLevel.Error);
                }

                var loRaPayloadJoinAccept = new LoRaPayloadJoinAccept(
                    LoRaTools.Utils.ConversionHelper.ByteArrayToString(netId), // NETID 0 / 1 is default test
                    ConversionHelper.StringToByteArray(devAddr),               // todo add device address management
                    appNonceBytes,
                    dlSettings,
                    rxDelay,
                    null);

                var joinAccept = loRaPayloadJoinAccept.Serialize(loRaDevice.AppKey, datr, freq, tmst, devEUI);
                if (joinAccept != null)
                {
                    _ = request.PacketForwarder.SendDownstreamAsync(joinAccept);
                    request.NotifySucceeded(loRaDevice, joinAccept);

                    if (Logger.LoggerLevel <= LogLevel.Debug)
                    {
                        var jsonMsg = JsonConvert.SerializeObject(joinAccept);
                        Logger.Log(devEUI, $"{LoRaMessageType.JoinAccept.ToString()} {jsonMsg}", LogLevel.Debug);
                    }
                    else if (Logger.LoggerLevel == LogLevel.Information)
                    {
                        Logger.Log(devEUI, "join accepted", LogLevel.Information);
                    }
                }
            }
            catch (Exception ex)
            {
                var deviceId = devEUI ?? ConversionHelper.ByteArrayToString(request.Payload.DevAddr);
                Logger.Log(deviceId, $"failed to handle join request. {ex.Message}", LogLevel.Error);
                request.NotifyFailed(loRaDevice, ex);
            }
        }
Example #28
0
 public void DispatchRequest(LoRaRequest request)
 {
     Task.Run(async() => await this.ProcessJoinRequestAsync(request));
 }
Example #29
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));
        }
Example #30
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);
        }