示例#1
0
        public MacCommandHolder GetMacCommands()
        {
            MacCommandHolder macHolder = new MacCommandHolder(Fopts.ToArray());

            return(macHolder);
        }
示例#2
0
        private async Task <byte[]> ProcessLoraMessage(LoRaMessageWrapper loraMessage)
        {
            bool validFrameCounter = false;

            byte[]  udpMsgForPktForwarder = new byte[0];
            string  devAddr = ConversionHelper.ByteArrayToString(loraMessage.LoRaPayloadMessage.DevAddr.ToArray());
            Message c2dMsg  = null;

            Cache.TryGetValue(devAddr, out LoraDeviceInfo loraDeviceInfo);

            if (loraDeviceInfo == null || !loraDeviceInfo.IsOurDevice)
            {
                loraDeviceInfo = await LoraDeviceInfoManager.GetLoraDeviceInfoAsync(devAddr, GatewayID);

                if (loraDeviceInfo.DevEUI != null)
                {
                    Logger.Log(loraDeviceInfo.DevEUI, $"processing message, device not in cache", Logger.LoggingLevel.Info);
                }
                else
                {
                    Logger.Log(devAddr, $"processing message, device not in cache", Logger.LoggingLevel.Info);
                }
                Cache.AddToCache(devAddr, loraDeviceInfo);
            }
            else
            {
                Logger.Log(loraDeviceInfo.DevEUI, $"processing message, device in cache", Logger.LoggingLevel.Info);
            }


            if (loraDeviceInfo != null && loraDeviceInfo.IsOurDevice)
            {
                //either there is no gateway linked to the device or the gateway is the one that the code is running
                if (String.IsNullOrEmpty(loraDeviceInfo.GatewayID) || loraDeviceInfo.GatewayID.ToUpper() == GatewayID.ToUpper())
                {
                    if (loraMessage.CheckMic(loraDeviceInfo.NwkSKey))
                    {
                        if (loraDeviceInfo.HubSender == null)
                        {
                            loraDeviceInfo.HubSender = new IoTHubConnector(loraDeviceInfo.DevEUI, loraDeviceInfo.PrimaryKey);
                        }
                        UInt16 fcntup = BitConverter.ToUInt16(loraMessage.LoRaPayloadMessage.GetLoRaMessage().Fcnt.ToArray(), 0);
                        byte[] linkCheckCmdResponse = null;

                        //check if the frame counter is valid: either is above the server one or is an ABP device resetting the counter (relaxed seqno checking)
                        if (fcntup > loraDeviceInfo.FCntUp || (fcntup == 0 && loraDeviceInfo.FCntUp == 0) || (fcntup == 1 && String.IsNullOrEmpty(loraDeviceInfo.AppEUI)))
                        {
                            //save the reset fcnt for ABP (relaxed seqno checking)
                            if (fcntup == 1 && String.IsNullOrEmpty(loraDeviceInfo.AppEUI))
                            {
                                _ = loraDeviceInfo.HubSender.UpdateFcntAsync(fcntup, 0, true);

                                //if the device is not attached to a gateway we need to reset the abp fcnt server side cache
                                if (String.IsNullOrEmpty(loraDeviceInfo.GatewayID))
                                {
                                    bool rit = await LoraDeviceInfoManager.ABPFcntCacheReset(loraDeviceInfo.DevEUI);
                                }
                            }

                            validFrameCounter = true;
                            Logger.Log(loraDeviceInfo.DevEUI, $"valid frame counter, msg: {fcntup} server: {loraDeviceInfo.FCntUp}", Logger.LoggingLevel.Info);

                            byte[] decryptedMessage = null;
                            try
                            {
                                decryptedMessage = loraMessage.DecryptPayload(loraDeviceInfo.AppSKey);
                            }
                            catch (Exception ex)
                            {
                                Logger.Log(loraDeviceInfo.DevEUI, $"failed to decrypt message: {ex.Message}", Logger.LoggingLevel.Error);
                            }
                            Rxpk    rxPk            = loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0];
                            dynamic fullPayload     = JObject.FromObject(rxPk);
                            string  jsonDataPayload = "";
                            uint    fportUp         = 0;
                            bool    isAckFromDevice = false;
                            if (loraMessage.LoRaPayloadMessage.GetLoRaMessage().Fport.Span.Length > 0)
                            {
                                fportUp = (uint)loraMessage.LoRaPayloadMessage.GetLoRaMessage().Fport.Span[0];
                            }
                            else // this is an acknowledgment sent from the device
                            {
                                isAckFromDevice       = true;
                                fullPayload.deviceAck = true;
                            }
                            fullPayload.port = fportUp;
                            fullPayload.fcnt = fcntup;

                            if (isAckFromDevice)
                            {
                                jsonDataPayload  = Convert.ToBase64String(decryptedMessage);
                                fullPayload.data = jsonDataPayload;
                            }
                            else
                            {
                                Logger.Log(loraDeviceInfo.DevEUI, $"decoding with: {loraDeviceInfo.SensorDecoder} port: {fportUp}", Logger.LoggingLevel.Info);
                                fullPayload.data = await LoraDecoders.DecodeMessage(decryptedMessage, fportUp, loraDeviceInfo.SensorDecoder);
                            }

                            fullPayload.eui       = loraDeviceInfo.DevEUI;
                            fullPayload.gatewayid = GatewayID;
                            //Edge timestamp
                            fullPayload.edgets = (long)((startTimeProcessing - new DateTime(1970, 1, 1)).TotalMilliseconds);
                            List <KeyValuePair <String, String> > messageProperties = new List <KeyValuePair <String, String> >();

                            //Parsing MacCommands and add them as property of the message to be sent to the IoT Hub.
                            var macCommand = ((LoRaPayloadData)loraMessage.LoRaPayloadMessage).GetMacCommands();
                            if (macCommand.macCommand.Count > 0)
                            {
                                for (int i = 0; i < macCommand.macCommand.Count; i++)
                                {
                                    messageProperties.Add(new KeyValuePair <string, string>(macCommand.macCommand[i].Cid.ToString(), value: JsonConvert.SerializeObject(macCommand.macCommand[i], Newtonsoft.Json.Formatting.None)));
                                    //in case it is a link check mac, we need to send it downstream.
                                    if (macCommand.macCommand[i].Cid == CidEnum.LinkCheckCmd)
                                    {
                                        linkCheckCmdResponse = new LinkCheckCmd(rxPk.GetModulationMargin(), 1).ToBytes();
                                    }
                                }
                            }
                            string iotHubMsg = fullPayload.ToString(Newtonsoft.Json.Formatting.None);
                            await loraDeviceInfo.HubSender.SendMessageAsync(iotHubMsg, messageProperties);

                            if (isAckFromDevice)
                            {
                                Logger.Log(loraDeviceInfo.DevEUI, $"ack from device sent to hub", Logger.LoggingLevel.Info);
                            }
                            else
                            {
                                var fullPayloadAsString = fullPayload.data as string;
                                if (fullPayloadAsString == null)
                                {
                                    fullPayloadAsString = ((JObject)fullPayload.data).ToString(Formatting.None);
                                }
                                Logger.Log(loraDeviceInfo.DevEUI, $"message '{fullPayloadAsString}' sent to hub", Logger.LoggingLevel.Info);
                            }
                            loraDeviceInfo.FCntUp = fcntup;
                        }
                        else
                        {
                            validFrameCounter = false;
                            Logger.Log(loraDeviceInfo.DevEUI, $"invalid frame counter, msg: {fcntup} server: {loraDeviceInfo.FCntUp}", Logger.LoggingLevel.Info);
                        }


                        //we lock as fast as possible and get the down fcnt for multi gateway support for confirmed message
                        if (loraMessage.LoRaMessageType == LoRaMessageType.ConfirmedDataUp && String.IsNullOrEmpty(loraDeviceInfo.GatewayID))
                        {
                            ushort newFCntDown = await LoraDeviceInfoManager.NextFCntDown(loraDeviceInfo.DevEUI, loraDeviceInfo.FCntDown, fcntup, GatewayID);

                            //ok to send down ack or msg
                            if (newFCntDown > 0)
                            {
                                loraDeviceInfo.FCntDown = newFCntDown;
                            }
                            //another gateway was first with this message we simply drop
                            else
                            {
                                PhysicalPayload pushAck = new PhysicalPayload(loraMessage.PhysicalPayload.token, PhysicalIdentifier.PUSH_ACK, null);
                                udpMsgForPktForwarder = pushAck.GetMessage();
                                Logger.Log(loraDeviceInfo.DevEUI, $"another gateway has already sent ack or downlink msg", Logger.LoggingLevel.Info);
                                Logger.Log(loraDeviceInfo.DevEUI, $"processing time: {DateTime.UtcNow - startTimeProcessing}", Logger.LoggingLevel.Info);
                                return(udpMsgForPktForwarder);
                            }
                        }
                        //start checking for new c2d message, we do it even if the fcnt is invalid so we support replying to the ConfirmedDataUp
                        //todo ronnie should we wait up to 900 msec?
                        c2dMsg = await loraDeviceInfo.HubSender.ReceiveAsync(TimeSpan.FromMilliseconds(20));

                        byte[] bytesC2dMsg = null;
                        byte[] fport       = null;
                        //Todo revamp fctrl
                        byte[] fctrl = new byte[1] {
                            32
                        };
                        //check if we got a c2d message to be added in the ack message and prepare the message
                        if (c2dMsg != null)
                        {
                            ////check if there is another message
                            var secondC2dMsg = await loraDeviceInfo.HubSender.ReceiveAsync(TimeSpan.FromMilliseconds(20));

                            if (secondC2dMsg != null)
                            {
                                //put it back to the queue for the next pickup
                                //todo ronnie check abbandon logic especially in case of mqtt
                                _ = await loraDeviceInfo.HubSender.AbandonAsync(secondC2dMsg);

                                //set the fpending flag so the lora device will call us back for the next message
                                fctrl = new byte[1] {
                                    48
                                };
                            }

                            bytesC2dMsg = c2dMsg.GetBytes();
                            fport       = new byte[1] {
                                1
                            };

                            if (bytesC2dMsg != null)
                            {
                                Logger.Log(loraDeviceInfo.DevEUI, $"C2D message: {Encoding.UTF8.GetString(bytesC2dMsg)}", Logger.LoggingLevel.Info);
                            }

                            //todo ronnie implement a better max payload size by datarate
                            //cut to the max payload of lora for any EU datarate
                            if (bytesC2dMsg.Length > 51)
                            {
                                Array.Resize(ref bytesC2dMsg, 51);
                            }

                            Array.Reverse(bytesC2dMsg);
                        }

                        //if confirmation or cloud to device msg send down the message
                        if (loraMessage.LoRaMessageType == LoRaMessageType.ConfirmedDataUp || c2dMsg != null)
                        {
                            //check if we are not too late for the second receive windows
                            if ((DateTime.UtcNow - startTimeProcessing) <= TimeSpan.FromMilliseconds(RegionFactory.CurrentRegion.receive_delay2 * 1000 - 100))
                            {
                                //if running in multigateway we need to use redis to sync the down fcnt
                                if (!String.IsNullOrEmpty(loraDeviceInfo.GatewayID))
                                {
                                    loraDeviceInfo.FCntDown++;
                                }
                                else if (loraMessage.LoRaMessageType == LoRaMessageType.UnconfirmedDataUp)
                                {
                                    ushort newFCntDown = await LoraDeviceInfoManager.NextFCntDown(loraDeviceInfo.DevEUI, loraDeviceInfo.FCntDown, fcntup, GatewayID);

                                    //ok to send down ack or msg
                                    if (newFCntDown > 0)
                                    {
                                        loraDeviceInfo.FCntDown = newFCntDown;
                                    }
                                    //another gateway was first with this message we simply drop
                                    else
                                    {
                                        PhysicalPayload pushAck = new PhysicalPayload(loraMessage.PhysicalPayload.token, PhysicalIdentifier.PUSH_ACK, null);
                                        udpMsgForPktForwarder = pushAck.GetMessage();
                                        Logger.Log(loraDeviceInfo.DevEUI, $"another gateway has already sent ack or downlink msg", Logger.LoggingLevel.Info);
                                        Logger.Log(loraDeviceInfo.DevEUI, $"processing time: {DateTime.UtcNow - startTimeProcessing}", Logger.LoggingLevel.Info);
                                        return(udpMsgForPktForwarder);
                                    }
                                }
                                Logger.Log(loraDeviceInfo.DevEUI, $"down frame counter: {loraDeviceInfo.FCntDown}", Logger.LoggingLevel.Info);

                                //Saving both fcnts to twins
                                _ = loraDeviceInfo.HubSender.UpdateFcntAsync(loraDeviceInfo.FCntUp, loraDeviceInfo.FCntDown);
                                //todo need implementation of current configuation to implement this as this depends on RX1DROffset
                                //var _datr = ((UplinkPktFwdMessage)loraMessage.LoraMetadata.FullPayload).rxpk[0].datr;
                                var datr = RegionFactory.CurrentRegion.GetDownstreamDR(loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0]);
                                //todo should discuss about the logic in case of multi channel gateway.
                                uint rfch = loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0].rfch;
                                //todo should discuss about the logic in case of multi channel gateway
                                double freq = RegionFactory.CurrentRegion.GetDownstreamChannel(loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0]);
                                //if we are already longer than 900 mssecond move to the 2 second window
                                //uncomment the following line to force second windows usage TODO change this to a proper expression?
                                //Thread.Sleep(901
                                long tmst = loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0].tmst + RegionFactory.CurrentRegion.receive_delay1 * 1000000;

                                if ((DateTime.UtcNow - startTimeProcessing) > TimeSpan.FromMilliseconds(RegionFactory.CurrentRegion.receive_delay1 * 1000 - 100))
                                {
                                    fctrl = new byte[1] {
                                        32
                                    };
                                    if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("RX2_DATR")))
                                    {
                                        Logger.Log(loraDeviceInfo.DevEUI, $"using standard second receive windows", Logger.LoggingLevel.Info);
                                        tmst = loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0].tmst + RegionFactory.CurrentRegion.receive_delay2 * 1000000;
                                        freq = RegionFactory.CurrentRegion.RX2DefaultReceiveWindows.frequency;
                                        datr = RegionFactory.CurrentRegion.DRtoConfiguration[RegionFactory.CurrentRegion.RX2DefaultReceiveWindows.dr].configuration;
                                    }
                                    //if specific twins are set, specify second channel to be as specified
                                    else
                                    {
                                        freq = double.Parse(Environment.GetEnvironmentVariable("RX2_FREQ"));
                                        datr = Environment.GetEnvironmentVariable("RX2_DATR");
                                        Logger.Log(loraDeviceInfo.DevEUI, $"using custom DR second receive windows freq : {freq}, datr:{datr}", Logger.LoggingLevel.Info);
                                    }
                                }
                                Byte[] devAddrCorrect = new byte[4];
                                Array.Copy(loraMessage.LoRaPayloadMessage.DevAddr.ToArray(), devAddrCorrect, 4);
                                Array.Reverse(devAddrCorrect);
                                bool requestForConfirmedResponse = false;

                                //check if the c2d message has a mac command
                                byte[] macbytes = null;
                                if (c2dMsg != null)
                                {
                                    var macCmd = c2dMsg.Properties.Where(o => o.Key == "CidType");
                                    if (macCmd.Count() != 0)
                                    {
                                        MacCommandHolder macCommandHolder = new MacCommandHolder(Convert.ToByte(macCmd.First().Value));
                                        macbytes = macCommandHolder.macCommand[0].ToBytes();
                                    }
                                    var confirmCmd = c2dMsg.Properties.Where(o => o.Key == "Confirmed");
                                    if (confirmCmd.Count() != 0)
                                    {
                                        requestForConfirmedResponse = true;
                                    }
                                }
                                if (requestForConfirmedResponse)
                                {
                                    fctrl[0] += 16;
                                }
                                if (macbytes != null && linkCheckCmdResponse != null)
                                {
                                    macbytes = macbytes.Concat(linkCheckCmdResponse).ToArray();
                                }
                                LoRaPayloadData ackLoRaMessage = new LoRaPayloadData(
                                    requestForConfirmedResponse ? MType.ConfirmedDataDown : MType.UnconfirmedDataDown,
                                    //ConversionHelper.StringToByteArray(requestForConfirmedResponse?"A0":"60"),
                                    devAddrCorrect,
                                    fctrl,
                                    BitConverter.GetBytes(loraDeviceInfo.FCntDown),
                                    macbytes,
                                    fport,
                                    bytesC2dMsg,
                                    1);

                                ackLoRaMessage.PerformEncryption(loraDeviceInfo.AppSKey);
                                ackLoRaMessage.SetMic(loraDeviceInfo.NwkSKey);

                                byte[] rndToken = new byte[2];
                                Random rnd      = new Random();
                                rnd.NextBytes(rndToken);
                                //todo ronnie should check the device twin preference if using confirmed or unconfirmed down
                                LoRaMessageWrapper ackMessage = new LoRaMessageWrapper(ackLoRaMessage, LoRaMessageType.UnconfirmedDataDown, rndToken, datr, 0, freq, tmst);
                                udpMsgForPktForwarder = ackMessage.PhysicalPayload.GetMessage();
                                linkCheckCmdResponse  = null;
                                //confirm the message to iot hub only if we are in time for a delivery
                                if (c2dMsg != null)
                                {
                                    //todo ronnie check if it is ok to do async so we make it in time to send the message
                                    _ = loraDeviceInfo.HubSender.CompleteAsync(c2dMsg);
                                    //bool rit = await loraDeviceInfo.HubSender.CompleteAsync(c2dMsg);
                                    //if (rit)
                                    //    Logger.Log(loraDeviceInfo.DevEUI, $"completed the c2d msg to IoT Hub", Logger.LoggingLevel.Info);
                                    //else
                                    //{
                                    //    //we could not complete the msg so we send only a pushAck
                                    //    PhysicalPayload pushAck = new PhysicalPayload(loraMessage.PhysicalPayload.token, PhysicalIdentifier.PUSH_ACK, null);
                                    //    udpMsgForPktForwarder = pushAck.GetMessage();
                                    //    Logger.Log(loraDeviceInfo.DevEUI, $"could not complete the c2d msg to IoT Hub", Logger.LoggingLevel.Info);

                                    //}
                                }
                            }
                            else
                            {
                                PhysicalPayload pushAck = new PhysicalPayload(loraMessage.PhysicalPayload.token, PhysicalIdentifier.PUSH_ACK, null);
                                udpMsgForPktForwarder = pushAck.GetMessage();

                                //put back the c2d message to the queue for the next round
                                //todo ronnie check abbandon logic especially in case of mqtt
                                if (c2dMsg != null)
                                {
                                    _ = await loraDeviceInfo.HubSender.AbandonAsync(c2dMsg);
                                }
                                Logger.Log(loraDeviceInfo.DevEUI, $"too late for down message, sending only ACK to gateway", Logger.LoggingLevel.Info);
                                _ = loraDeviceInfo.HubSender.UpdateFcntAsync(loraDeviceInfo.FCntUp, null);
                            }
                        }
                        //No ack requested and no c2d message we send the udp ack only to the gateway
                        else if (loraMessage.LoRaMessageType == LoRaMessageType.UnconfirmedDataUp && c2dMsg == null)
                        {
                            PhysicalPayload pushAck = new PhysicalPayload(loraMessage.PhysicalPayload.token, PhysicalIdentifier.PUSH_ACK, null);
                            udpMsgForPktForwarder = pushAck.GetMessage();

                            ////if ABP and 1 we reset the counter (loose frame counter) with force, if not we update normally
                            //if (fcntup == 1 && String.IsNullOrEmpty(loraDeviceInfo.AppEUI))
                            //    _ = loraDeviceInfo.HubSender.UpdateFcntAsync(fcntup, null, true);
                            if (validFrameCounter)
                            {
                                _ = loraDeviceInfo.HubSender.UpdateFcntAsync(loraDeviceInfo.FCntUp, null);
                            }
                        }

                        //If there is a link check command waiting
                        if (linkCheckCmdResponse != null)
                        {
                            Byte[] devAddrCorrect = new byte[4];
                            Array.Copy(loraMessage.LoRaPayloadMessage.DevAddr.ToArray(), devAddrCorrect, 4);
                            byte[] fctrl2 = new byte[1] {
                                32
                            };

                            Array.Reverse(devAddrCorrect);
                            LoRaPayloadData macReply = new LoRaPayloadData(MType.ConfirmedDataDown,
                                                                           devAddrCorrect,
                                                                           fctrl2,
                                                                           BitConverter.GetBytes(loraDeviceInfo.FCntDown),
                                                                           null,
                                                                           new byte[1] {
                                0
                            },
                                                                           linkCheckCmdResponse,
                                                                           1);

                            macReply.PerformEncryption(loraDeviceInfo.AppSKey);
                            macReply.SetMic(loraDeviceInfo.NwkSKey);

                            byte[] rndToken = new byte[2];
                            Random rnd      = new Random();
                            rnd.NextBytes(rndToken);

                            var datr = RegionFactory.CurrentRegion.GetDownstreamDR(loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0]);
                            //todo should discuss about the logic in case of multi channel gateway.
                            uint   rfch = loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0].rfch;
                            double freq = RegionFactory.CurrentRegion.GetDownstreamChannel(loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0]);
                            long   tmst = loraMessage.PktFwdPayload.GetPktFwdMessage().Rxpks[0].tmst + RegionFactory.CurrentRegion.receive_delay1 * 1000000;
                            //todo ronnie should check the device twin preference if using confirmed or unconfirmed down
                            LoRaMessageWrapper ackMessage = new LoRaMessageWrapper(macReply, LoRaMessageType.UnconfirmedDataDown, rndToken, datr, 0, freq, tmst);
                            udpMsgForPktForwarder = ackMessage.PhysicalPayload.GetMessage();
                        }
                    }
                    else
                    {
                        Logger.Log(loraDeviceInfo.DevEUI, $"with devAddr {devAddr} check MIC failed. Device will be ignored from now on", Logger.LoggingLevel.Info);
                        loraDeviceInfo.IsOurDevice = false;
                    }
                }
                else
                {
                    Logger.Log(loraDeviceInfo.DevEUI, $"ignore message because is not linked to this GatewayID", Logger.LoggingLevel.Info);
                }
            }
            else
            {
                Logger.Log(devAddr, $"device is not our device, ignore message", Logger.LoggingLevel.Info);
            }



            Logger.Log(loraDeviceInfo.DevEUI, $"processing time: {DateTime.UtcNow - startTimeProcessing}", Logger.LoggingLevel.Info);


            return(udpMsgForPktForwarder);
        }
        /// <summary>
        /// Creates downlink message with ack for confirmation or cloud to device message
        /// </summary>
        DownlinkPktFwdMessage CreateDownlinkMessage(
            Message cloudToDeviceMessage,
            LoRaDevice loRaDevice,
            Rxpk rxpk,
            LoRaPayloadData upstreamPayload,
            LoRaOperationTimeWatcher timeWatcher,
            ReadOnlyMemory <byte> payloadDevAddr,
            bool fpending,
            ushort fcntDown)
        {
            // default fport
            byte fctrl = 0;

            if (upstreamPayload.LoRaMessageType == LoRaMessageType.ConfirmedDataUp)
            {
                // Confirm receiving message to device
                fctrl = (byte)FctrlEnum.Ack;
            }

            byte?fport = null;
            var  requiresDeviceAcknowlegement = false;

            byte[] macbytes = null;

            byte[] rndToken = new byte[2];
            Random rnd      = new Random();

            rnd.NextBytes(rndToken);

            byte[] frmPayload = null;

            if (cloudToDeviceMessage != null)
            {
                if (cloudToDeviceMessage.Properties.TryGetValueCaseInsensitive("cidtype", out var cidTypeValue))
                {
                    Logger.Log(loRaDevice.DevEUI, "Cloud to device MAC command received", LogLevel.Information);
                    MacCommandHolder macCommandHolder = new MacCommandHolder(Convert.ToByte(cidTypeValue));
                    macbytes = macCommandHolder.MacCommand[0].ToBytes();
                }

                if (cloudToDeviceMessage.Properties.TryGetValueCaseInsensitive("confirmed", out var confirmedValue) && confirmedValue.Equals("true", StringComparison.OrdinalIgnoreCase))
                {
                    requiresDeviceAcknowlegement         = true;
                    loRaDevice.LastConfirmedC2DMessageID = cloudToDeviceMessage.MessageId ?? C2D_MSG_ID_PLACEHOLDER;
                }

                if (cloudToDeviceMessage.Properties.TryGetValueCaseInsensitive("fport", out var fPortValue))
                {
                    fport = byte.Parse(fPortValue);
                }

                Logger.Log(loRaDevice.DevEUI, $"Sending a downstream message with ID {ConversionHelper.ByteArrayToString(rndToken)}", LogLevel.Debug);

                frmPayload = cloudToDeviceMessage?.GetBytes();

                Logger.Log(loRaDevice.DevEUI, $"C2D message: {Encoding.UTF8.GetString(frmPayload)}, id: {cloudToDeviceMessage.MessageId ?? "undefined"}, fport: {fport}, confirmed: {requiresDeviceAcknowlegement}, cidType: {cidTypeValue}", LogLevel.Information);

                // cut to the max payload of lora for any EU datarate
                if (frmPayload.Length > 51)
                {
                    Array.Resize(ref frmPayload, 51);
                }

                Array.Reverse(frmPayload);
            }

            if (fpending)
            {
                fctrl |= (int)FctrlEnum.FpendingOrClassB;
            }

            // if (macbytes != null && linkCheckCmdResponse != null)
            //     macbytes = macbytes.Concat(linkCheckCmdResponse).ToArray();
            var reversedDevAddr = new byte[payloadDevAddr.Length];
            var srcDevAddr      = payloadDevAddr.Span;

            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(fcntDown),
                macbytes,
                fport.HasValue ? new byte[] { fport.Value } : null,
                frmPayload,
                1);

            // var firstWindowTime = timeWatcher.GetRemainingTimeToReceiveFirstWindow(loRaDevice);
            // if (firstWindowTime > TimeSpan.Zero)
            //     System.Threading.Thread.Sleep(firstWindowTime);
            var receiveWindow = timeWatcher.ResolveReceiveWindowToUse(loRaDevice);

            if (receiveWindow == 0)
            {
                return(null);
            }

            string datr = null;
            double freq = 0;
            long   tmst = 0;

            if (receiveWindow == 2)
            {
                tmst = rxpk.Tmst + timeWatcher.GetReceiveWindow2Delay(loRaDevice) * 1000000;

                if (string.IsNullOrEmpty(this.configuration.Rx2DataRate))
                {
                    Logger.Log(loRaDevice.DevEUI, "using standard second receive windows", LogLevel.Information);
                    freq = this.loraRegion.RX2DefaultReceiveWindows.frequency;
                    datr = this.loraRegion.DRtoConfiguration[this.loraRegion.RX2DefaultReceiveWindows.dr].configuration;
                }

                // if specific twins are set, specify second channel to be as specified
                else
                {
                    freq = this.configuration.Rx2DataFrequency;
                    datr = this.configuration.Rx2DataRate;
                    Logger.Log(loRaDevice.DevEUI, $"using custom DR second receive windows freq : {freq}, datr:{datr}", LogLevel.Information);
                }
            }
            else
            {
                try
                {
                    datr = this.loraRegion.GetDownstreamDR(rxpk);
                    freq = this.loraRegion.GetDownstreamChannelFrequency(rxpk);
                    tmst = rxpk.Tmst + this.loraRegion.Receive_delay1 * 1000000;
                }
                catch (RegionLimitException ex)
                {
                    Logger.Log(loRaDevice.DevEUI, ex.Message, LogLevel.Error);
                    return(null);
                }
            }

            // todo: check the device twin preference if using confirmed or unconfirmed down
            return(ackLoRaMessage.Serialize(loRaDevice.AppSKey, loRaDevice.NwkSKey, datr, freq, tmst, loRaDevice.DevEUI));
        }