private static DownlinkMessage BuildDownstreamMessage(LoRaDevice loRaDevice, StationEui stationEUI, ILogger logger, ulong xTime, ReceiveWindow?rx1, ReceiveWindow rx2, RxDelay lnsRxDelay, LoRaPayloadData loRaMessage, LoRaDeviceClassType deviceClassType, uint?antennaPreference = null) { var messageBytes = loRaMessage.Serialize(loRaDevice.AppSKey.Value, loRaDevice.NwkSKey.Value); var downlinkMessage = new DownlinkMessage( messageBytes, xTime, rx1, rx2, loRaDevice.DevEUI, lnsRxDelay, deviceClassType, stationEUI, antennaPreference ); if (logger.IsEnabled(LogLevel.Debug)) { logger.LogDebug($"{loRaMessage.MessageType} {JsonConvert.SerializeObject(downlinkMessage)}"); } return(downlinkMessage); }
public DownlinkMessage(byte[] payload, ulong xtime, ReceiveWindow?rx1, ReceiveWindow rx2, DevEui devEui, RxDelay lnsRxDelay, LoRaDeviceClassType deviceClassType, StationEui stationEui = default, uint?antennaPreference = null) { if (payload is null) { throw new ArgumentNullException(nameof(payload)); } Data = payload; DevEui = devEui; LnsRxDelay = lnsRxDelay; DeviceClassType = deviceClassType; AntennaPreference = antennaPreference; Rx1 = rx1; Rx2 = rx2; StationEui = stationEui; Xtime = xtime; }
public void ProcessReceive(Context ctx) { if (_nextTransmitBlockNumber != 0) { return; } var signals = ReceiveWindow.OrderByDescending(s => s.SignalStrength).ToList(); ReceiveWindow.Clear(); if (signals.Count == 0) { if (_receiveBuffer.Count != 0) { var first = _receiveBuffer[0]; if (_receiveBuffer.Count == _parameters.TransmissionLengthTicks && _receiveBuffer.All(s => s.Transmission == first.Transmission) && _receiveBuffer.All(s => s.SignalSource == first.SignalSource)) { if (!_seenMessages.Contains(first.Transmission)) { _seenMessages.Add(first.Transmission); _transmitQueue.Enqueue(first.Transmission); } } _receiveBuffer.Clear(); } ++_ticksFromLastReceivedBlock; return; } _ticksFromLastReceivedBlock = 0; if (signals.Count > 1 && signals[0].SignalStrength / signals[1].SignalStrength < _parameters.SnrCutoff) { return; } if (signals.Count != 0) { _receiveBuffer.Add(signals[0]); return; } }
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; } }
/// <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)); }