void SendClassCDeviceMessage(IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage) { if (this.classCDeviceMessageSender != null) { Task.Run(() => this.classCDeviceMessageSender.SendAsync(cloudToDeviceMessage)); } }
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)); }
/// <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)); }
internal static DownlinkMessageBuilderResponse CreateDownlinkMessage( NetworkServerConfiguration configuration, LoRaDevice loRaDevice, Region loRaRegion, IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage, uint fcntDown) { var fcntDownToSend = ValidateAndConvert16bitFCnt(fcntDown); // default fport byte fctrl = 0; CidEnum macCommandType = CidEnum.Zero; byte[] rndToken = new byte[2]; lock (RndLock) { RndDownlinkMessageBuilder.NextBytes(rndToken); } bool isMessageTooLong = false; // Class C always uses RX2 string datr; double freq; var tmst = 0; // immediate mode // Class C always use RX2 (freq, datr) = loRaRegion.GetDownstreamRX2DRAndFreq(loRaDevice.DevEUI, configuration.Rx2DataRate, configuration.Rx2DataFrequency, loRaDevice.ReportedRX2DataRate); // 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 = PrepareMacCommandAnswer(loRaDevice.DevEUI, null, cloudToDeviceMessage.MacCommands, null, null); // Calculate total C2D payload size var totalC2dSize = cloudToDeviceMessage.GetPayload()?.Length ?? 0; totalC2dSize += macCommands?.Sum(x => x.Length) ?? 0; // Total C2D payload will NOT fit if (availablePayloadSize < totalC2dSize) { isMessageTooLong = true; return(new DownlinkMessageBuilderResponse(null, isMessageTooLong)); } if (macCommands?.Count > 0) { macCommandType = macCommands.First().Cid; } if (cloudToDeviceMessage.Confirmed) { loRaDevice.LastConfirmedC2DMessageID = cloudToDeviceMessage.MessageId ?? Constants.C2D_MSG_ID_PLACEHOLDER; } var frmPayload = cloudToDeviceMessage.GetPayload(); if (Logger.LoggerLevel >= LogLevel.Information) { Logger.Log(loRaDevice.DevEUI, $"cloud to device message: {ConversionHelper.ByteArrayToString(frmPayload)}, id: {cloudToDeviceMessage.MessageId ?? "undefined"}, fport: {cloudToDeviceMessage.Fport}, confirmed: {cloudToDeviceMessage.Confirmed}, cidType: {macCommandType}", LogLevel.Information); Logger.Log(loRaDevice.DevEUI, $"sending a downstream message with ID {ConversionHelper.ByteArrayToString(rndToken)}", LogLevel.Information); } Array.Reverse(frmPayload); var payloadDevAddr = ConversionHelper.StringToByteArray(loRaDevice.DevAddr); var reversedDevAddr = new byte[payloadDevAddr.Length]; for (int i = reversedDevAddr.Length - 1; i >= 0; --i) { reversedDevAddr[i] = payloadDevAddr[payloadDevAddr.Length - (1 + i)]; } var msgType = cloudToDeviceMessage.Confirmed ? LoRaMessageType.ConfirmedDataDown : LoRaMessageType.UnconfirmedDataDown; var ackLoRaMessage = new LoRaPayloadData( msgType, reversedDevAddr, new byte[] { fctrl }, BitConverter.GetBytes(fcntDownToSend), macCommands, new byte[] { cloudToDeviceMessage.Fport }, frmPayload, 1, loRaDevice.Supports32BitFCnt ? fcntDown : (uint?)null); return(new DownlinkMessageBuilderResponse( ackLoRaMessage.Serialize(loRaDevice.AppSKey, loRaDevice.NwkSKey, datr, freq, tmst, loRaDevice.DevEUI), isMessageTooLong)); }
/// <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)); }
internal static DownlinkMessageBuilderResponse CreateDownlinkMessage( NetworkServerConfiguration configuration, LoRaDevice loRaDevice, Region loRaRegion, IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage, uint fcntDown, ILogger logger) { var fcntDownToSend = ValidateAndConvert16bitFCnt(fcntDown); // default fport var macCommandType = Cid.Zero; var rndToken = new byte[2]; RndKeysGenerator.GetBytes(rndToken); var isMessageTooLong = false; var deviceJoinInfo = loRaRegion.LoRaRegion == LoRaRegionType.CN470RP2 ? new DeviceJoinInfo(loRaDevice.ReportedCN470JoinChannel, loRaDevice.DesiredCN470JoinChannel) : null; // Class C always uses RX2 DataRateIndex datr; Hertz freq; // Class C always use RX2 freq = loRaRegion.GetDownstreamRX2Freq(configuration.Rx2Frequency, deviceJoinInfo, logger); datr = loRaRegion.GetDownstreamRX2DataRate(configuration.Rx2DataRate, loRaDevice.ReportedRX2DataRate, 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 = PrepareMacCommandAnswer(null, cloudToDeviceMessage.MacCommands, null, null, logger); // Calculate total C2D payload size var totalC2dSize = cloudToDeviceMessage.GetPayload()?.Length ?? 0; totalC2dSize += macCommands?.Sum(x => x.Length) ?? 0; // Total C2D payload will NOT fit if (availablePayloadSize < totalC2dSize) { isMessageTooLong = true; return(new DownlinkMessageBuilderResponse(null, isMessageTooLong, ReceiveWindow2)); } if (macCommands?.Count > 0) { macCommandType = macCommands.First().Cid; } if (cloudToDeviceMessage.Confirmed) { loRaDevice.LastConfirmedC2DMessageID = cloudToDeviceMessage.MessageId ?? Constants.C2D_MSG_ID_PLACEHOLDER; } var frmPayload = cloudToDeviceMessage.GetPayload(); if (logger.IsEnabled(LogLevel.Information)) { logger.LogInformation($"cloud to device message: {frmPayload.ToHex()}, id: {cloudToDeviceMessage.MessageId ?? "undefined"}, fport: {cloudToDeviceMessage.Fport}, confirmed: {cloudToDeviceMessage.Confirmed}, cidType: {macCommandType}"); } var msgType = cloudToDeviceMessage.Confirmed ? MacMessageType.ConfirmedDataDown : MacMessageType.UnconfirmedDataDown; var ackLoRaMessage = new LoRaPayloadData( msgType, loRaDevice.DevAddr.Value, FrameControlFlags.None, fcntDownToSend, macCommands, cloudToDeviceMessage.Fport, frmPayload, 1, loRaDevice.Supports32BitFCnt ? fcntDown : null); var loraDownLinkMessage = BuildDownstreamMessage(loRaDevice: loRaDevice, stationEUI: loRaDevice.LastProcessingStationEui, logger: logger, xTime: 0, null, new ReceiveWindow(datr, freq), RxDelay0, ackLoRaMessage, LoRaDeviceClassType.C); if (logger.IsEnabled(LogLevel.Debug)) { logger.LogDebug($"{ackLoRaMessage.MessageType} {JsonConvert.SerializeObject(loraDownLinkMessage)}"); } // Class C always uses RX2. return(new DownlinkMessageBuilderResponse(loraDownLinkMessage, isMessageTooLong, ReceiveWindow2)); }
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); }
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); } } } }
public async Task <bool> SendAsync(IReceivedLoRaCloudToDeviceMessage message, CancellationToken cts = default) { if (message is null) { throw new ArgumentNullException(nameof(message)); } var devEui = message.DevEUI.GetValueOrDefault(); if (!devEui.IsValid) { this.logger.LogError($"[class-c] devEUI missing/invalid in payload"); return(false); } if (!message.IsValid(out var validationErrorMessage)) { this.logger.LogError($"[class-c] {validationErrorMessage}"); return(false); } var loRaDevice = await this.loRaDeviceRegistry.GetDeviceByDevEUIAsync(devEui); if (loRaDevice == null) { this.logger.LogError($"[class-c] device {message.DevEUI} not found or not joined"); return(false); } if (!RegionManager.TryTranslateToRegion(loRaDevice.LoRaRegion, out var region)) { this.logger.LogError("[class-c] device does not have a region assigned. Ensure the device has connected at least once with the network"); return(false); } if (cts.IsCancellationRequested) { this.logger.LogError($"[class-c] device {message.DevEUI} timed out, stopping"); return(false); } if (loRaDevice.DevAddr is null) { this.logger.LogError("[class-c] devAddr is empty, cannot send cloud to device message. Ensure the device has connected at least once with the network"); return(false); } if (loRaDevice.ClassType != LoRaDeviceClassType.C) { this.logger.LogError($"[class-c] sending cloud to device messages expects a class C device. Class type is {loRaDevice.ClassType}"); return(false); } if (loRaDevice.LastProcessingStationEui == default) { this.logger.LogError("[class-c] sending cloud to device messages expects a class C device already connected to one station and reported its StationEui. No StationEui was saved for this device."); return(false); } var frameCounterStrategy = this.frameCounterUpdateStrategyProvider.GetStrategy(loRaDevice.GatewayID); if (frameCounterStrategy == null) { this.logger.LogError($"[class-c] could not resolve frame count update strategy for device, gateway id: {loRaDevice.GatewayID}"); return(false); } var fcntDown = await frameCounterStrategy.NextFcntDown(loRaDevice, 0); if (fcntDown <= 0) { this.logger.LogError("[class-c] could not obtain fcnt down for class C device"); return(false); } this.logger.LogDebug($"[class-c] down frame counter: {loRaDevice.FCntDown}"); var downlinkMessageBuilderResp = DownlinkMessageBuilder.CreateDownlinkMessage( this.configuration, loRaDevice, // TODO resolve region from device information region, message, fcntDown, this.logger); var messageIdLog = message.MessageId ?? "undefined"; if (downlinkMessageBuilderResp.IsMessageTooLong) { this.c2dMessageTooLong?.Add(1); this.logger.LogError($"[class-c] cloud to device message too large, rejecting. Id: {messageIdLog}"); if (!await message.RejectAsync()) { this.logger.LogError($"[class-c] failed to reject. Id: {messageIdLog}"); } return(false); } else { try { await this.downstreamMessageSender.SendDownstreamAsync(downlinkMessageBuilderResp.DownlinkMessage); } catch (Exception ex) { this.logger.LogError($"[class-c] failed to send the message, abandoning. Id: {messageIdLog}, ex: {ex.Message}"); if (!await message.AbandonAsync()) { this.logger.LogError($"[class-c] failed to abandon the message. Id: {messageIdLog}"); } throw; } if (!await message.CompleteAsync()) { this.logger.LogError($"[class-c] failed to complete the message. Id: {messageIdLog}"); } if (!await frameCounterStrategy.SaveChangesAsync(loRaDevice)) { this.logger.LogWarning("[class-c] failed to update framecounter."); } } return(true); }
public async Task <bool> SendAsync(IReceivedLoRaCloudToDeviceMessage cloudToDeviceMessage, CancellationToken cts = default(CancellationToken)) { try { if (string.IsNullOrEmpty(cloudToDeviceMessage.DevEUI)) { Logger.Log($"[class-c] devEUI missing in payload", LogLevel.Error); return(false); } if (!cloudToDeviceMessage.IsValid(out var validationErrorMessage)) { Logger.Log(cloudToDeviceMessage.DevEUI, $"[class-c] {validationErrorMessage}", LogLevel.Error); return(false); } var loRaDevice = await this.loRaDeviceRegistry.GetDeviceByDevEUIAsync(cloudToDeviceMessage.DevEUI); if (loRaDevice == null) { Logger.Log(cloudToDeviceMessage.DevEUI, $"[class-c] device {cloudToDeviceMessage.DevEUI} not found", LogLevel.Error); return(false); } if (!RegionManager.TryTranslateToRegion(loRaDevice.LoRaRegion, out var region)) { Logger.Log(cloudToDeviceMessage.DevEUI, $"[class-c] device does not have a region assigned. Ensure the device has connected at least once with the network", LogLevel.Error); return(false); } if (cts.IsCancellationRequested) { Logger.Log(cloudToDeviceMessage.DevEUI, $"[class-c] device {cloudToDeviceMessage.DevEUI} timed out, stopping", LogLevel.Error); return(false); } if (string.IsNullOrEmpty(loRaDevice.DevAddr)) { Logger.Log(loRaDevice.DevEUI, "[class-c] devAddr is empty, cannot send cloud to device message. Ensure the device has connected at least once with the network", LogLevel.Error); return(false); } if (loRaDevice.ClassType != LoRaDeviceClassType.C) { Logger.Log(loRaDevice.DevEUI, $"[class-c] sending cloud to device messages expects a class C device. Class type is {loRaDevice.ClassType}", LogLevel.Error); return(false); } var frameCounterStrategy = this.frameCounterUpdateStrategyProvider.GetStrategy(loRaDevice.GatewayID); if (frameCounterStrategy == null) { Logger.Log(loRaDevice.DevEUI, $"[class-c] could not resolve frame count update strategy for device, gateway id: {loRaDevice.GatewayID}", LogLevel.Error); return(false); } var fcntDown = await frameCounterStrategy.NextFcntDown(loRaDevice, 0); if (fcntDown <= 0) { Logger.Log(loRaDevice.DevEUI, "[class-c] could not obtain fcnt down for class C device", LogLevel.Error); return(false); } Logger.Log(loRaDevice.DevEUI, $"[class-c] down frame counter: {loRaDevice.FCntDown}", LogLevel.Debug); var downlinkMessageBuilderResp = DownlinkMessageBuilder.CreateDownlinkMessage( this.configuration, loRaDevice, // TODO resolve region from device information region, cloudToDeviceMessage, fcntDown); if (downlinkMessageBuilderResp.IsMessageTooLong) { Logger.Log(loRaDevice.DevEUI, $"[class-c] cloud to device message too large, rejecting. Id: {cloudToDeviceMessage.MessageId ?? "undefined"}", LogLevel.Error); await cloudToDeviceMessage.RejectAsync(); return(false); } else { await this.packetForwarder.SendDownstreamAsync(downlinkMessageBuilderResp.DownlinkPktFwdMessage); await frameCounterStrategy.SaveChangesAsync(loRaDevice); } return(true); } catch (Exception ex) { Logger.Log(cloudToDeviceMessage.DevEUI, $"[class-c] error sending class C cloud to device message. {ex.Message}", LogLevel.Error); return(false); } }
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);