public async Task OTAA_Confirmed_Message_Should_Send_Data_To_IotHub_Update_FcntUp_And_Return_DownstreamMessage() { var simulatedDevice = new SimulatedDevice(TestDeviceInfo.CreateABPDevice(1, gatewayID: this.ServerConfiguration.GatewayID)); var payload = simulatedDevice.CreateConfirmedDataUpMessage("1234"); // Create Rxpk var rxpk = payload.SerializeUplink(simulatedDevice.AppSKey, simulatedDevice.NwkSKey).Rxpk[0]; var loraDeviceClient = new Mock <ILoRaDeviceClient>(); var loraDevice = TestUtils.CreateFromSimulatedDevice(simulatedDevice, loraDeviceClient.Object); loraDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull <LoRaDeviceTelemetry>(), null)) .ReturnsAsync(true); var payloadDecoder = new Mock <ILoRaPayloadDecoder>(); this.LoRaDeviceRegistry.Setup(x => x.GetDeviceForPayloadAsync(It.IsAny <LoRaTools.LoRaMessage.LoRaPayloadData>())) .ReturnsAsync(loraDevice); // Setup frame counter strategy this.FrameCounterUpdateStrategyFactory.Setup(x => x.GetSingleGatewayStrategy()) .Returns(new SingleGatewayFrameCounterUpdateStrategy()); // Frame counter will be asked to save changes this.FrameCounterUpdateStrategy.Setup(x => x.SaveChangesAsync(loraDevice)).ReturnsAsync(true); // Send to message processor var messageProcessor = new MessageProcessor( this.ServerConfiguration, this.LoRaDeviceRegistry.Object, this.FrameCounterUpdateStrategyFactory.Object, payloadDecoder.Object); var actual = await messageProcessor.ProcessMessageAsync(rxpk); // Expectations // 1. Message was sent to IoT Hub loraDeviceClient.VerifyAll(); // 2. Single gateway frame counter strategy was used this.FrameCounterUpdateStrategyFactory.VerifyAll(); // 3. Return is downstream message Assert.NotNull(actual); Assert.IsType <DownlinkPktFwdMessage>(actual); var downlinkMessage = (DownlinkPktFwdMessage)actual; var payloadDataDown = new LoRaPayloadData(Convert.FromBase64String(downlinkMessage.Txpk.Data)); Assert.Equal(payloadDataDown.DevAddr.ToArray(), LoRaTools.Utils.ConversionHelper.StringToByteArray(loraDevice.DevAddr)); Assert.False(payloadDataDown.IsConfirmed()); Assert.Equal(LoRaMessageType.UnconfirmedDataDown, payloadDataDown.LoRaMessageType); // 4. Frame counter up was updated Assert.Equal(1, loraDevice.FCntUp); // 5. Frame counter down was incremented Assert.Equal(1, loraDevice.FCntDown); Assert.Equal(1, MemoryMarshal.Read <ushort>(payloadDataDown.Fcnt.Span)); }
public async Task OTAA_Confirmed_With_Cloud_To_Device_Message_Returns_Downstream_Message() { const int PayloadFcnt = 10; const int InitialDeviceFcntUp = 9; const int InitialDeviceFcntDown = 20; var simulatedDevice = new SimulatedDevice( TestDeviceInfo.CreateABPDevice(1, gatewayID: this.ServerConfiguration.GatewayID), frmCntUp: InitialDeviceFcntUp, frmCntDown: InitialDeviceFcntDown); var loraDeviceClient = new Mock <ILoRaDeviceClient>(MockBehavior.Strict); var loraDevice = TestUtils.CreateFromSimulatedDevice(simulatedDevice, loraDeviceClient.Object); loraDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull <LoRaDeviceTelemetry>(), null)) .ReturnsAsync(true); loraDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsNotNull <TwinCollection>())) .ReturnsAsync(true); var cloudToDeviceMessage = new Message(Encoding.UTF8.GetBytes("c2d")); cloudToDeviceMessage.Properties[MessageProcessor.FPORT_MSG_PROPERTY_KEY] = "1"; loraDeviceClient.SetupSequence(x => x.ReceiveAsync(It.IsAny <TimeSpan>())) .ReturnsAsync(cloudToDeviceMessage) .ReturnsAsync((Message)null); // 2nd cloud to device message does not return anything loraDeviceClient.Setup(x => x.CompleteAsync(cloudToDeviceMessage)) .ReturnsAsync(true); var payloadDecoder = new Mock <ILoRaPayloadDecoder>(); this.LoRaDeviceRegistry.Setup(x => x.GetDeviceForPayloadAsync(It.IsAny <LoRaTools.LoRaMessage.LoRaPayloadData>())) .ReturnsAsync(loraDevice); // Setup frame counter strategy this.FrameCounterUpdateStrategyFactory.Setup(x => x.GetSingleGatewayStrategy()) .Returns(new SingleGatewayFrameCounterUpdateStrategy()); // Send to message processor var messageProcessor = new MessageProcessor( this.ServerConfiguration, this.LoRaDeviceRegistry.Object, this.FrameCounterUpdateStrategyFactory.Object, payloadDecoder.Object); var payload = simulatedDevice.CreateConfirmedDataUpMessage("1234", fcnt: PayloadFcnt); var rxpk = payload.SerializeUplink(simulatedDevice.AppSKey, simulatedDevice.NwkSKey).Rxpk[0]; var actual = await messageProcessor.ProcessMessageAsync(rxpk); // Expectations // 1. Message was sent to IoT Hub loraDeviceClient.VerifyAll(); // 2. Single gateway frame counter strategy was used this.FrameCounterUpdateStrategyFactory.VerifyAll(); // 3. Return is downstream message Assert.NotNull(actual); Assert.IsType <DownlinkPktFwdMessage>(actual); var downlinkMessage = (DownlinkPktFwdMessage)actual; var payloadDataDown = new LoRaPayloadData(Convert.FromBase64String(downlinkMessage.Txpk.Data)); payloadDataDown.PerformEncryption(loraDevice.AppSKey); Assert.Equal(payloadDataDown.DevAddr.ToArray(), LoRaTools.Utils.ConversionHelper.StringToByteArray(loraDevice.DevAddr)); Assert.False(payloadDataDown.IsConfirmed()); Assert.Equal(LoRaMessageType.UnconfirmedDataDown, payloadDataDown.LoRaMessageType); // 4. Frame counter up was updated Assert.Equal(PayloadFcnt, loraDevice.FCntUp); // 5. Frame counter down is updated Assert.Equal(InitialDeviceFcntDown + 1, loraDevice.FCntDown); Assert.Equal(InitialDeviceFcntDown + 1, payloadDataDown.GetFcnt()); // 6. Frame count has no pending changes Assert.False(loraDevice.HasFrameCountChanges); }
public async Task When_Device_Prefers_Second_Window_Should_Send_Downstream_In_Second_Window() { const int PayloadFcnt = 10; const int InitialDeviceFcntUp = 9; const int InitialDeviceFcntDown = 20; var simulatedDevice = new SimulatedDevice( TestDeviceInfo.CreateABPDevice(1, gatewayID: this.ServerConfiguration.GatewayID), frmCntUp: InitialDeviceFcntUp, frmCntDown: InitialDeviceFcntDown); var devAddr = simulatedDevice.DevAddr; var devEUI = simulatedDevice.DevEUI; var loraDeviceClient = new Mock <ILoRaDeviceClient>(MockBehavior.Strict); // Will get twin to initialize LoRaDevice var deviceTwin = TestUtils.CreateABPTwin( simulatedDevice, desiredProperties: new Dictionary <string, object> { { TwinProperty.PreferredWindow, 2 } }); loraDeviceClient.Setup(x => x.GetTwinAsync()).ReturnsAsync(deviceTwin); loraDeviceClient.Setup(x => x.SendEventAsync(It.IsNotNull <LoRaDeviceTelemetry>(), null)) .ReturnsAsync(true); loraDeviceClient.Setup(x => x.UpdateReportedPropertiesAsync(It.IsNotNull <TwinCollection>())) .ReturnsAsync(true); var cloudToDeviceMessage = new Message(Encoding.UTF8.GetBytes("c2d")); cloudToDeviceMessage.Properties[MessageProcessor.FPORT_MSG_PROPERTY_KEY] = "1"; loraDeviceClient.SetupSequence(x => x.ReceiveAsync(It.IsAny <TimeSpan>())) .ReturnsAsync(cloudToDeviceMessage) .Returns(this.EmptyAdditionalMessageReceiveAsync); // 2nd cloud to device message does not return anything loraDeviceClient.Setup(x => x.CompleteAsync(cloudToDeviceMessage)) .ReturnsAsync(true); var payloadDecoder = new Mock <ILoRaPayloadDecoder>(); var loRaDeviceAPI = new Mock <LoRaDeviceAPIServiceBase>(); loRaDeviceAPI.Setup(x => x.SearchByDevAddrAsync(devAddr)) .ReturnsAsync(new SearchDevicesResult(new IoTHubDeviceInfo(devAddr, devEUI, "adad").AsList())); var loRaDeviceRegistry = new LoRaDeviceRegistry(this.ServerConfiguration, new MemoryCache(new MemoryCacheOptions()), loRaDeviceAPI.Object, new TestLoRaDeviceFactory(loraDeviceClient.Object)); // Send to message processor var messageProcessor = new MessageProcessor( this.ServerConfiguration, loRaDeviceRegistry, new LoRaDeviceFrameCounterUpdateStrategyFactory(this.ServerConfiguration.GatewayID, loRaDeviceAPI.Object), new LoRaPayloadDecoder()); var payload = simulatedDevice.CreateUnconfirmedDataUpMessage("1234", fcnt: PayloadFcnt); var rxpk = payload.SerializeUplink(simulatedDevice.AppSKey, simulatedDevice.NwkSKey).Rxpk[0]; var actual = await messageProcessor.ProcessMessageAsync(rxpk); // Expectations // 1. Message was sent to IoT Hub loraDeviceClient.VerifyAll(); // 2. Single gateway frame counter strategy was used this.FrameCounterUpdateStrategyFactory.VerifyAll(); // 3. Return is downstream message Assert.NotNull(actual); Assert.IsType <DownlinkPktFwdMessage>(actual); var downlinkMessage = (DownlinkPktFwdMessage)actual; var euRegion = RegionFactory.CreateEU868Region(); // Ensure we are using second window frequency Assert.Equal(euRegion.RX2DefaultReceiveWindows.frequency, actual.Txpk.Freq); // Ensure we are using second window datr Assert.Equal(euRegion.DRtoConfiguration[euRegion.RX2DefaultReceiveWindows.dr].configuration, actual.Txpk.Datr); // Ensure tmst was computed to 2 seconds (2 windows in Europe) Assert.Equal(2000000, actual.Txpk.Tmst); // Get the device from registry var deviceDictionary = loRaDeviceRegistry.InternalGetCachedDevicesForDevAddr(devAddr); Assert.True(deviceDictionary.TryGetValue(simulatedDevice.DevEUI, out var loRaDevice)); var payloadDataDown = new LoRaPayloadData(Convert.FromBase64String(downlinkMessage.Txpk.Data)); payloadDataDown.PerformEncryption(loRaDevice.AppSKey); Assert.Equal(payloadDataDown.DevAddr.ToArray(), ConversionHelper.StringToByteArray(loRaDevice.DevAddr)); Assert.False(payloadDataDown.IsConfirmed()); Assert.Equal(LoRaMessageType.UnconfirmedDataDown, payloadDataDown.LoRaMessageType); // 4. Frame counter up was updated Assert.Equal(PayloadFcnt, loRaDevice.FCntUp); // 5. Frame counter down is updated var expectedFcntDown = InitialDeviceFcntDown + 10 + 1; // adding 10 as buffer when creating a new device instance Assert.Equal(expectedFcntDown, loRaDevice.FCntDown); Assert.Equal(expectedFcntDown, payloadDataDown.GetFcnt()); // 6. Frame count has no pending changes Assert.False(loRaDevice.HasFrameCountChanges); }
/// <summary> /// Process LoRa message where the payload is of type LoRaPayloadData /// </summary> async Task <DownlinkPktFwdMessage> ProcessDataMessageAsync(LoRaTools.LoRaPhysical.Rxpk rxpk, LoRaPayloadData loraPayload, DateTime startTime) { var devAddr = loraPayload.DevAddr; var timeWatcher = new LoRaOperationTimeWatcher(this.loraRegion, startTime); using (var processLogger = new ProcessLogger(timeWatcher, devAddr)) { if (!this.IsValidNetId(loraPayload.GetDevAddrNetID(), this.configuration.NetId)) { Logger.Log(ConversionHelper.ByteArrayToString(devAddr), "device is using another network id, ignoring this message", LogLevel.Debug); processLogger.LogLevel = LogLevel.Debug; return(null); } // Find device that matches: // - devAddr // - mic check (requires: loraDeviceInfo.NwkSKey or loraDeviceInfo.AppKey, rxpk.LoraPayload.Mic) // - gateway id var loRaDevice = await this.deviceRegistry.GetDeviceForPayloadAsync(loraPayload); if (loRaDevice == null) { Logger.Log(ConversionHelper.ByteArrayToString(devAddr), $"device is not from our network, ignoring message", LogLevel.Information); return(null); } // Add context to logger processLogger.SetDevEUI(loRaDevice.DevEUI); var isMultiGateway = !string.Equals(loRaDevice.GatewayID, this.configuration.GatewayID, StringComparison.InvariantCultureIgnoreCase); var frameCounterStrategy = isMultiGateway ? this.frameCounterUpdateStrategyFactory.GetMultiGatewayStrategy() : this.frameCounterUpdateStrategyFactory.GetSingleGatewayStrategy(); var payloadFcnt = loraPayload.GetFcnt(); var requiresConfirmation = loraPayload.IsConfirmed(); using (new LoRaDeviceFrameCounterSession(loRaDevice, frameCounterStrategy)) { // Leaf devices that restart lose the counter. In relax mode we accept the incoming frame counter // ABP device does not reset the Fcnt so in relax mode we should reset for 0 (LMIC based) or 1 var isFrameCounterFromNewlyStartedDevice = false; if (payloadFcnt <= 1) { if (loRaDevice.IsABP) { if (loRaDevice.IsABPRelaxedFrameCounter && loRaDevice.FCntUp >= 0 && payloadFcnt <= 1) { // known problem when device restarts, starts fcnt from zero _ = frameCounterStrategy.ResetAsync(loRaDevice); isFrameCounterFromNewlyStartedDevice = true; } } else if (loRaDevice.FCntUp == payloadFcnt && payloadFcnt == 0) { // Some devices start with frame count 0 isFrameCounterFromNewlyStartedDevice = true; } } // Reply attack or confirmed reply // Confirmed resubmit: A confirmed message that was received previously but we did not answer in time // Device will send it again and we just need to return an ack (but also check for C2D to send it over) var isConfirmedResubmit = false; if (!isFrameCounterFromNewlyStartedDevice && payloadFcnt <= loRaDevice.FCntUp) { // if it is confirmed most probably we did not ack in time before or device lost the ack packet so we should continue but not send the msg to iothub if (requiresConfirmation && payloadFcnt == loRaDevice.FCntUp) { if (!loRaDevice.ValidateConfirmResubmit(payloadFcnt)) { Logger.Log(loRaDevice.DevEUI, $"resubmit from confirmed message exceeds threshold of {LoRaDevice.MaxConfirmationResubmitCount}, message ignored, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Debug); processLogger.LogLevel = LogLevel.Debug; return(null); } isConfirmedResubmit = true; Logger.Log(loRaDevice.DevEUI, $"resubmit from confirmed message detected, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Information); } else { Logger.Log(loRaDevice.DevEUI, $"invalid frame counter, message ignored, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Information); return(null); } } var fcntDown = 0; // If it is confirmed it require us to update the frame counter down // Multiple gateways: in redis, otherwise in device twin if (requiresConfirmation) { fcntDown = await frameCounterStrategy.NextFcntDown(loRaDevice, payloadFcnt); // Failed to update the fcnt down // In multi gateway scenarios it means the another gateway was faster than using, can stop now if (fcntDown <= 0) { // update our fcntup anyway? // loRaDevice.SetFcntUp(payloadFcnt); Logger.Log(loRaDevice.DevEUI, "another gateway has already sent ack or downlink msg", LogLevel.Information); return(null); } Logger.Log(loRaDevice.DevEUI, $"down frame counter: {loRaDevice.FCntDown}", LogLevel.Information); } if (!isConfirmedResubmit) { var validFcntUp = isFrameCounterFromNewlyStartedDevice || (payloadFcnt > loRaDevice.FCntUp); if (validFcntUp) { Logger.Log(loRaDevice.DevEUI, $"valid frame counter, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Information); object payloadData = null; // if it is an upward acknowledgement from the device it does not have a payload // This is confirmation from leaf device that he received a C2D confirmed // if a message payload is null we don't try to decrypt it. if (loraPayload.Frmpayload.Length != 0) { byte[] decryptedPayloadData = null; try { decryptedPayloadData = loraPayload.GetDecryptedPayload(loRaDevice.AppSKey); } catch (Exception ex) { Logger.Log(loRaDevice.DevEUI, $"failed to decrypt message: {ex.Message}", LogLevel.Error); } var fportUp = loraPayload.GetFPort(); if (string.IsNullOrEmpty(loRaDevice.SensorDecoder)) { Logger.Log(loRaDevice.DevEUI, $"no decoder set in device twin. port: {fportUp}", LogLevel.Debug); payloadData = Convert.ToBase64String(decryptedPayloadData); } else { Logger.Log(loRaDevice.DevEUI, $"decoding with: {loRaDevice.SensorDecoder} port: {fportUp}", LogLevel.Debug); payloadData = await this.payloadDecoder.DecodeMessageAsync(decryptedPayloadData, fportUp, loRaDevice.SensorDecoder); } } if (!await this.SendDeviceEventAsync(loRaDevice, rxpk, payloadData, loraPayload, timeWatcher)) { // failed to send event to IoT Hub, stop now return(null); } loRaDevice.SetFcntUp(payloadFcnt); } else { Logger.Log(loRaDevice.DevEUI, $"invalid frame counter, msg: {payloadFcnt} server: {loRaDevice.FCntUp}", LogLevel.Information); } } // We check if we have time to futher progress or not // C2D checks are quite expensive so if we are really late we just stop here var timeToSecondWindow = timeWatcher.GetRemainingTimeToReceiveSecondWindow(loRaDevice); if (timeToSecondWindow < LoRaOperationTimeWatcher.ExpectedTimeToPackageAndSendMessage) { if (requiresConfirmation) { Logger.Log(loRaDevice.DevEUI, $"too late for down message ({timeWatcher.GetElapsedTime()}), sending only ACK to gateway", LogLevel.Information); } return(null); } // If it is confirmed and // - Downlink is disabled for the device or // - we don't have time to check c2d and send to device we return now if (requiresConfirmation && (!loRaDevice.DownlinkEnabled || timeToSecondWindow.Subtract(LoRaOperationTimeWatcher.ExpectedTimeToPackageAndSendMessage) <= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage)) { return(this.CreateDownlinkMessage( null, loRaDevice, rxpk, loraPayload, timeWatcher, devAddr, false, // fpending (ushort)fcntDown)); } // Flag indicating if there is another C2D message waiting var fpending = false; // Contains the Cloud to message we need to send Message cloudToDeviceMessage = null; if (loRaDevice.DownlinkEnabled) { // ReceiveAsync has a longer timeout // But we wait less that the timeout (available time before 2nd window) // if message is received after timeout, keep it in loraDeviceInfo and return the next call var timeAvailableToCheckCloudToDeviceMessages = timeWatcher.GetAvailableTimeToCheckCloudToDeviceMessage(loRaDevice); if (timeAvailableToCheckCloudToDeviceMessages >= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage) { cloudToDeviceMessage = await loRaDevice.ReceiveCloudToDeviceAsync(timeAvailableToCheckCloudToDeviceMessages); if (cloudToDeviceMessage != null && !this.ValidateCloudToDeviceMessage(loRaDevice, cloudToDeviceMessage)) { _ = loRaDevice.CompleteCloudToDeviceMessageAsync(cloudToDeviceMessage); cloudToDeviceMessage = null; } if (cloudToDeviceMessage != null) { if (!requiresConfirmation) { // The message coming from the device was not confirmed, therefore we did not computed the frame count down // Now we need to increment because there is a C2D message to be sent fcntDown = await frameCounterStrategy.NextFcntDown(loRaDevice, payloadFcnt); if (fcntDown == 0) { // We did not get a valid frame count down, therefore we should not process the message _ = loRaDevice.AbandonCloudToDeviceMessageAsync(cloudToDeviceMessage); cloudToDeviceMessage = null; } else { requiresConfirmation = true; } Logger.Log(loRaDevice.DevEUI, $"down frame counter: {loRaDevice.FCntDown}", LogLevel.Information); } // Checking again if cloudToDeviceMessage is valid because the fcntDown resolution could have failed, // causing us to drop the message if (cloudToDeviceMessage != null) { var remainingTimeForFPendingCheck = timeWatcher.GetRemainingTimeToReceiveSecondWindow(loRaDevice) - (LoRaOperationTimeWatcher.CheckForCloudMessageCallEstimatedOverhead + LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage); if (remainingTimeForFPendingCheck >= LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage) { var additionalMsg = await loRaDevice.ReceiveCloudToDeviceAsync(LoRaOperationTimeWatcher.MinimumAvailableTimeToCheckForCloudMessage); if (additionalMsg != null) { fpending = true; _ = loRaDevice.AbandonCloudToDeviceMessageAsync(additionalMsg); Logger.Log(loRaDevice.DevEUI, $"found fpending c2d message id: {additionalMsg.MessageId ?? "undefined"}", LogLevel.Information); } } } } } } // No C2D message and request was not confirmed, return nothing if (!requiresConfirmation) { return(null); } var confirmDownstream = this.CreateDownlinkMessage( cloudToDeviceMessage, loRaDevice, rxpk, loraPayload, timeWatcher, devAddr, fpending, (ushort)fcntDown); if (cloudToDeviceMessage != null) { if (confirmDownstream == null) { Logger.Log(loRaDevice.DevEUI, $"out of time for downstream message, will abandon c2d message id: {cloudToDeviceMessage.MessageId ?? "undefined"}", LogLevel.Information); _ = loRaDevice.AbandonCloudToDeviceMessageAsync(cloudToDeviceMessage); } else { _ = loRaDevice.CompleteCloudToDeviceMessageAsync(cloudToDeviceMessage); } } return(confirmDownstream); } } }