/// <summary> /// Process OTAA join request /// </summary> async Task <DownlinkPktFwdMessage> ProcessJoinRequestAsync(Rxpk rxpk, LoRaPayloadJoinRequest joinReq, DateTime startTimeProcessing) { var timeWatcher = new LoRaOperationTimeWatcher(this.loraRegion, startTimeProcessing); using (var processLogger = new ProcessLogger(timeWatcher)) { byte[] udpMsgForPktForwarder = new byte[0]; var devEUI = joinReq.GetDevEUIAsString(); var appEUI = joinReq.GetAppEUIAsString(); // set context to logger processLogger.SetDevEUI(devEUI); var devNonce = joinReq.GetDevNonceAsString(); Logger.Log(devEUI, $"join request received", LogLevel.Information); var loRaDevice = await this.deviceRegistry.GetDeviceForJoinRequestAsync(devEUI, appEUI, devNonce); if (loRaDevice == null) { return(null); } if (string.IsNullOrEmpty(loRaDevice.AppKey)) { Logger.Log(loRaDevice.DevEUI, "join refused: missing AppKey for OTAA device", LogLevel.Error); return(null); } if (loRaDevice.AppEUI != appEUI) { Logger.Log(devEUI, "join refused: AppEUI for OTAA does not match device", LogLevel.Error); return(null); } if (!joinReq.CheckMic(loRaDevice.AppKey)) { Logger.Log(devEUI, "join refused: invalid MIC", LogLevel.Error); return(null); } // Make sure that is a new request and not a replay if (!string.IsNullOrEmpty(loRaDevice.DevNonce) && loRaDevice.DevNonce == devNonce) { Logger.Log(devEUI, "join refused: DevNonce already used by this device", LogLevel.Information); loRaDevice.IsOurDevice = false; return(null); } // 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; return(null); } 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); 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); return(null); } Logger.Log(loRaDevice.DevEUI, $"saving join properties twins", LogLevel.Debug); var deviceUpdateSucceeded = await loRaDevice.UpdateAfterJoinAsync(devAddr, nwkSKey, appSKey, appNonce, devNonce, LoRaTools.Utils.ConversionHelper.ByteArrayToString(netId)); Logger.Log(loRaDevice.DevEUI, $"done saving join properties twins", LogLevel.Debug); if (!deviceUpdateSucceeded) { Logger.Log(devEUI, $"join refused: join request could not save twins", LogLevel.Error); return(null); } var windowToUse = timeWatcher.ResolveJoinAcceptWindowToUse(loRaDevice); if (windowToUse == 0) { Logger.Log(devEUI, $"join refused: processing of the join request took too long, sending no message", LogLevel.Information); return(null); } double freq = 0; string datr = null; uint tmst = 0; if (windowToUse == 1) { try { datr = this.loraRegion.GetDownstreamDR(rxpk); freq = this.loraRegion.GetDownstreamChannelFrequency(rxpk); } catch (RegionLimitException ex) { Logger.Log(devEUI, ex.ToString(), LogLevel.Error); } // set tmst for the normal case tmst = rxpk.Tmst + this.loraRegion.Join_accept_delay1 * 1000000; } else { Logger.Log(devEUI, $"processing of the join request took too long, using second join accept receive window", LogLevel.Information); tmst = rxpk.Tmst + this.loraRegion.Join_accept_delay2 * 1000000; if (string.IsNullOrEmpty(this.configuration.Rx2DataRate)) { Logger.Log(devEUI, $"using standard second receive windows for join request", LogLevel.Information); // using EU fix DR for RX2 freq = this.loraRegion.RX2DefaultReceiveWindows.frequency; datr = this.loraRegion.DRtoConfiguration[RegionFactory.CurrentRegion.RX2DefaultReceiveWindows.dr].configuration; } else { Logger.Log(devEUI, $"using custom second receive windows for join request", LogLevel.Information); freq = this.configuration.Rx2DataFrequency; datr = this.configuration.Rx2DataRate; } } loRaDevice.IsOurDevice = true; this.deviceRegistry.UpdateDeviceAfterJoin(loRaDevice); // Build join accept downlink message Array.Reverse(netId); Array.Reverse(appNonceBytes); return(this.CreateJoinAcceptDownlinkMessage( netId, loRaDevice.AppKey, devAddr, appNonceBytes, datr, freq, tmst, devEUI)); } }
/// <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); } } }