public async Task Data_Rate_Is_Updated_When_C2D_With_LinkADRCmd_Received() { const int messagesToSend = 10; const int warmUpMessageCount = 2; var device = TestFixtureCi.Device32_ABP; LogTestStart(device); // Setup LoRa device properties await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP); await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, device.AppEui); await ArduinoDevice.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey); // Setup protocol properties // Start with DR5 await ArduinoDevice.SetupLora(TestFixtureCi.Configuration.LoraRegion, LoRaArduinoSerial._data_rate_t.DR5, 4, true); await TestFixture.CleanupC2DDeviceQueueAsync(device.DeviceID); // Sends 2x unconfirmed messages for (var i = 1; i <= warmUpMessageCount; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/{messagesToSend}"); await ArduinoDevice.transferPacketAsync(msg, 10); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); TestFixture.ClearLogs(); } var c2dMessage = new LoRaCloudToDeviceMessage() { Fport = FramePort.MacCommand, Payload = String.Empty, MacCommands = { new LinkADRRequest(datarate: 3, txPower: 4, chMask: 25, chMaskCntl: 0, nbTrans: 1) } // Update data rate to DR3 }; await TestFixtureCi.SendCloudToDeviceMessageAsync(device.DeviceID, c2dMessage); Log($"C2D Message sent to device, need to check if it receives"); var foundC2DMessage = false; var foundLinkADRCmd = false; var foundChangedDataRate = false; // Sends 8x unconfirmed messages, stopping if C2D message is found and data rate is updated for (var i = warmUpMessageCount + 1; i <= messagesToSend; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/{messagesToSend}"); await ArduinoDevice.transferPacketAsync(msg, 10); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); // After transferPacket: Expectation from serial // +MSG: Done await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); // check if C2D message was found if (await SearchMessageAsync($"{device.DeviceID}: cloud to device MAC command LinkADRCmd received Type: LinkADRCmd Answer, datarate: 3")) { foundC2DMessage = true; } // check if LinkADRCmd MAC Command was detected if (await SearchMessageAsync("LinkADRCmd mac command detected in upstream payload: Type: LinkADRCmd Answer, power: changed, data rate: changed")) { foundLinkADRCmd = true; } // check if the data rate was changed to DR3 if (await SearchMessageAsync("\"datr\":\"SF9BW125\"")) { foundChangedDataRate = true; } async Task <bool> SearchMessageAsync(string message) { var searchResult = await TestFixtureCi.SearchNetworkServerModuleAsync(messageBody => messageBody.Contains(message, StringComparison.OrdinalIgnoreCase), new SearchLogOptions(message) { MaxAttempts = 1 }); return(searchResult.Found); } TestFixture.ClearLogs(); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); if (foundC2DMessage && foundLinkADRCmd && foundChangedDataRate) { break; } } Assert.True(foundC2DMessage, $"Could not find C2D message with MAC command LinkADRCmd in LNS log"); Assert.True(foundLinkADRCmd, $"Could not find LinkADRCmd MAC Command in LNS log"); Assert.True(foundChangedDataRate, $"Could not find updated data rate in LNS log"); }
private async Task Test_OTAA_Unconfirmed_Send_And_Receive_C2D_Mac_CommandsImplAsync(TestDeviceInfo device, string c2dMessageBody) { const int MaxAttempts = 5; const int UnconfirmedMsgCount = 2; // Sends 2x unconfirmed messages for (var i = 1; i <= UnconfirmedMsgCount; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/{UnconfirmedMsgCount}"); await ArduinoDevice.transferPacketAsync(msg, 10); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); TestFixtureCi.ClearLogs(); } var c2dMessage = new LoRaCloudToDeviceMessage() { Fport = FramePorts.App1, Payload = c2dMessageBody, MacCommands = { new DevStatusRequest() } }; await TestFixtureCi.SendCloudToDeviceMessageAsync(device.DeviceID, c2dMessage); Log($"Message {c2dMessageBody} sent to device, need to check if it receives"); var macCommandReceivedMsg = $"{device.DeviceID}: cloud to device MAC command DevStatusCmd received"; var foundMacCommandReceivedMsg = false; var deviceMacCommandResponseMsg = $": DevStatusCmd mac command detected in upstream payload: Type: DevStatusCmd Answer, Battery Level:"; var foundDeviceMacCommandResponseMsg = false; // Sends 5x unconfirmed messages, stopping if assertions succeeded for (var i = 1; i <= MaxAttempts; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/{MaxAttempts}"); await ArduinoDevice.transferPacketAsync(msg, 10); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); // check if c2d message was found if (!foundMacCommandReceivedMsg) { var searchResults = await TestFixtureCi.SearchNetworkServerModuleAsync( (messageBody) => { return(messageBody.StartsWith(macCommandReceivedMsg, StringComparison.Ordinal)); }, new SearchLogOptions(macCommandReceivedMsg) { MaxAttempts = 1 }); // We should only receive the message once if (searchResults.Found) { foundMacCommandReceivedMsg = true; Log($"{device.DeviceID}: Found Mac C2D message in log (after sending {i}/{MaxAttempts}) ? {foundMacCommandReceivedMsg}"); } } if (!foundDeviceMacCommandResponseMsg) { var macSearchResults = await TestFixtureCi.SearchNetworkServerModuleAsync( (messageBody) => { return(messageBody.Contains(deviceMacCommandResponseMsg, StringComparison.InvariantCultureIgnoreCase)); }, new SearchLogOptions(deviceMacCommandResponseMsg) { MaxAttempts = 1 }); // We should only receive the message once if (macSearchResults.Found) { foundDeviceMacCommandResponseMsg = true; Log($"{device.DeviceID}: Found Mac Command reply in log (after sending {i}/{MaxAttempts}) ? {foundDeviceMacCommandResponseMsg}"); } } TestFixtureCi.ClearLogs(); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); if (foundDeviceMacCommandResponseMsg && foundMacCommandReceivedMsg) { break; } } Assert.True(foundMacCommandReceivedMsg, $"Did not find in network server logs: '{macCommandReceivedMsg}'"); Assert.True(foundDeviceMacCommandResponseMsg, $"Did not find in network server logs: '{deviceMacCommandResponseMsg}'"); }
public async Task Test_OTAA_Confirmed_Receives_C2D_Message_With_RX_Delay_2() { const int messagesToSend = 10; const int warmUpMessageCount = 2; var device = TestFixtureCi.Device9_OTAA; LogTestStart(device); await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA); await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, device.AppEui); await ArduinoDevice.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey); await ArduinoDevice.SetupLora(TestFixtureCi.Configuration); await TestFixture.CleanupC2DDeviceQueueAsync(device.DeviceID); var joinSucceeded = await ArduinoDevice.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5); Assert.True(joinSucceeded, "Join failed"); // find the gateway that accepted the join const string expectedLog = "JoinAccept"; var joinAccept = await TestFixtureCi.SearchNetworkServerModuleAsync((s) => s.IndexOf(expectedLog, StringComparison.OrdinalIgnoreCase) != -1, new SearchLogOptions(expectedLog)); Assert.NotNull(joinAccept); Assert.NotNull(joinAccept.MatchedEvent); var targetGw = joinAccept.MatchedEvent.SourceId; Assert.Equal(device.GatewayID, targetGw); // wait 1 second after joined await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN); // Sends 2x confirmed messages for (var i = 1; i <= warmUpMessageCount; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending confirmed '{msg}' {i}/{messagesToSend}"); await ArduinoDevice.transferPacketWithConfirmedAsync(msg, 10); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); // +CMSG: ACK Received await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", ArduinoDevice.SerialLogs); TestFixtureCi.ClearLogs(); } // sends C2D - between 10 and 99 var c2dMessageBody = (100 + random.Next(90)).ToString(CultureInfo.InvariantCulture); var c2dMessage = new LoRaCloudToDeviceMessage() { Payload = c2dMessageBody, Fport = FramePorts.App1, MessageId = Guid.NewGuid().ToString(), }; await TestFixtureCi.SendCloudToDeviceMessageAsync(device.DeviceID, c2dMessage); Log($"Message {c2dMessageBody} sent to device, need to check if it receives"); var foundC2DMessageCount = 0; var foundReceivePacketCount = 0; var expectedRxSerial = $"+CMSG: PORT: 1; RX: \"{ToHexString(c2dMessageBody)}\""; Log($"Expected C2D received log is: {expectedRxSerial}"); var c2dLogMessage = $"{device.DeviceID}: done completing cloud to device message, id: {c2dMessage.MessageId}"; Log($"Expected C2D network server log is: {expectedRxSerial}"); // Sends 8x confirmed messages, stopping if C2D message is found for (var i = warmUpMessageCount + 1; i <= messagesToSend; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending confirmed '{msg}' {i}/{messagesToSend}"); await ArduinoDevice.transferPacketWithConfirmedAsync(msg, 10); await Task.Delay(TimeSpan.FromSeconds(5)); // After transferPacketWithConfirmed: Expectation from serial // +CMSG: ACK Received await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", ArduinoDevice.SerialLogs); // Check that RXDelay was correctly used await TestFixtureCi.CheckAnswerTimingAsync(device.RXDelay, device.GatewayID); // check if c2d message was found // 0000000000000009: C2D message: 58 var searchResults = await TestFixtureCi.SearchNetworkServerModuleAsync( (messageBody) => { return(messageBody.StartsWith(c2dLogMessage, StringComparison.OrdinalIgnoreCase)); }, new SearchLogOptions(c2dLogMessage) { MaxAttempts = 1 }); // We should only receive the message once if (searchResults.Found) { foundC2DMessageCount++; Log($"{device.DeviceID}: Found C2D message in log (after sending {i}/{messagesToSend}) {foundC2DMessageCount} times"); EnsureNotSeenTooManyTimes(foundC2DMessageCount); } var localFoundCloudToDeviceInSerial = ArduinoDevice.SerialLogs.Contains(expectedRxSerial); if (localFoundCloudToDeviceInSerial) { foundReceivePacketCount++; Log($"{device.DeviceID}: Found C2D message in serial logs (after sending {i}/{messagesToSend}) {foundReceivePacketCount} times"); EnsureNotSeenTooManyTimes(foundReceivePacketCount); } TestFixtureCi.ClearLogs(); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); } Assert.True(foundC2DMessageCount > 0, $"Did not find '{device.DeviceID}: C2D message: {c2dMessageBody}' in logs"); // checks if log arrived if (foundReceivePacketCount == 0) { if (ArduinoDevice.SerialLogs.Contains(expectedRxSerial)) { foundReceivePacketCount++; } } Assert.True(foundReceivePacketCount > 0, $"Could not find lora receiving message '{expectedRxSerial}'"); }
public async Task C2D_When_Device_Has_Preferred_Windows_2_Should_Receive_In_2nd_Window_With_Custom_DR() { const int messagesToSend = 10; const int warmUpMessageCount = 2; var device = TestFixtureCi.Device21_ABP; LogTestStart(device); // Setup LoRa device properties await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP); await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, device.AppEui); await ArduinoDevice.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey); // Setup protocol properties await ArduinoDevice.SetupLora(TestFixture.Configuration); await TestFixture.CleanupC2DDeviceQueueAsync(device.DeviceID); // Sends 2x unconfirmed messages for (var i = 1; i <= warmUpMessageCount; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/{messagesToSend}"); await ArduinoDevice.transferPacketAsync(msg, 10); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); TestFixture.ClearLogs(); } // sends C2D - between 10 and 99 var c2dMessageBody = (100 + random.Next(90)).ToString(CultureInfo.InvariantCulture); var msgId = Guid.NewGuid().ToString(); var c2dMessage = new LoRaCloudToDeviceMessage() { Payload = c2dMessageBody, Fport = FramePorts.App1, MessageId = msgId, Confirmed = true, }; await TestFixtureCi.SendCloudToDeviceMessageAsync(device.DeviceID, c2dMessage); Log($"Message {c2dMessageBody} sent to device, need to check if it receives"); var foundC2DMessageCount = 0; var foundReceivePacketCount = 0; var foundReceivePacketInRX2Count = 0; var expectedRxSerial1 = $"+MSG: PORT: 1; RX: \"{ToHexString(c2dMessageBody)}\""; var expectedRxSerial2 = $"+MSG: RXWIN2"; var expectedTcpMessageV1 = $"{device.DevAddr}: ConfirmedDataDown"; var expectedTcpMessageV2 = $"{device.DeviceID}: cloud to device message: {ToHexString(c2dMessageBody)}, id: {msgId}, fport: 1, confirmed: True"; Log($"Expected C2D received log is: {expectedRxSerial1} and {expectedRxSerial2}"); Log($"Expected TCP log starting with: {expectedTcpMessageV1} or {expectedTcpMessageV2}"); // Sends 8x confirmed messages, stopping if C2D message is found for (var i = warmUpMessageCount + 1; i <= messagesToSend; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/{messagesToSend}"); await ArduinoDevice.transferPacketAsync(msg, 10); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); // check if c2d message was found var searchResults = await TestFixture.SearchNetworkServerModuleAsync( (messageBody) => { return(messageBody.StartsWith(expectedTcpMessageV1, StringComparison.OrdinalIgnoreCase) || messageBody.StartsWith(expectedTcpMessageV2, StringComparison.OrdinalIgnoreCase)); }, new SearchLogOptions($"{expectedTcpMessageV1} or {expectedTcpMessageV2}") { MaxAttempts = 1, }); // We should only receive the message once if (searchResults.Found) { foundC2DMessageCount++; Log($"{device.DeviceID}: Found C2D message in log (after sending {i}/{messagesToSend}) {foundC2DMessageCount} times"); EnsureNotSeenTooManyTimes(foundC2DMessageCount); } if (ArduinoDevice.SerialLogs.Contains(expectedRxSerial1)) { foundReceivePacketCount++; Log($"{device.DeviceID}: Found C2D message in serial logs (after sending {i}/{messagesToSend}) {foundReceivePacketCount} times"); EnsureNotSeenTooManyTimes(foundReceivePacketCount); } if (ArduinoDevice.SerialLogs.Any(x => x.StartsWith(expectedRxSerial2, StringComparison.OrdinalIgnoreCase))) { foundReceivePacketInRX2Count++; Log($"{device.DeviceID}: Found C2D message (rx2) in serial logs (after sending {i}/{messagesToSend}) {foundReceivePacketInRX2Count} times"); EnsureNotSeenTooManyTimes(foundReceivePacketInRX2Count); } if (foundReceivePacketCount > 0 && foundReceivePacketInRX2Count > 0 && foundC2DMessageCount > 0) { Log($"{device.DeviceID}: Found all messages in log (after sending {i}/{messagesToSend})"); break; } TestFixture.ClearLogs(); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); } Assert.True(foundC2DMessageCount > 0, $"Did not find {expectedTcpMessageV1} or {expectedTcpMessageV2} in logs"); // checks if serial received the message if (foundReceivePacketCount == 0) { if (ArduinoDevice.SerialLogs.Contains(expectedRxSerial1)) { foundReceivePacketCount++; } } Assert.True(foundReceivePacketCount > 0, $"Could not find lora receiving message '{expectedRxSerial1}'"); // checks if serial received the message in RX2 if (foundReceivePacketInRX2Count == 0) { if (ArduinoDevice.SerialLogs.Any(x => x.StartsWith(expectedRxSerial2, StringComparison.OrdinalIgnoreCase))) { foundReceivePacketInRX2Count++; } } Assert.True(foundReceivePacketInRX2Count > 0, $"Could not find lora receiving message '{expectedRxSerial2}'"); }
/* Commented multi gateway tests as they make C2D tests flaky for now * [RetryFact] * public Task Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message_MultiGw() * { * var device = TestFixtureCi.GetDeviceByPropertyName(nameof(TestFixtureCi.Device14_OTAA_MultiGw)); * LogTestStart(device); * return Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message(device); * } */ // Ensures that C2D messages are received when working with unconfirmed messages // Uses Device10_OTAA private async Task Test_OTAA_Unconfirmed_Receives_Confirmed_C2D_Message(TestDeviceInfo device) { const int messagesToSend = 10; const int warmUpMessageCount = 2; await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWOTAA); await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, device.AppEui); await ArduinoDevice.setKeyAsync(device.NwkSKey, device.AppSKey, device.AppKey); await ArduinoDevice.SetupLora(TestFixtureCi.Configuration); await TestFixture.CleanupC2DDeviceQueueAsync(device.DeviceID); var joinSucceeded = await ArduinoDevice.setOTAAJoinAsyncWithRetry(LoRaArduinoSerial._otaa_join_cmd_t.JOIN, 20000, 5); Assert.True(joinSucceeded, "Join failed"); // wait 1 second after joined await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_JOIN); if (device.IsMultiGw) { await TestFixtureCi.WaitForTwinSyncAfterJoinAsync(ArduinoDevice.SerialLogs, device.DevEui); } // Sends 2x unconfirmed messages for (var i = 1; i <= warmUpMessageCount; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/{messagesToSend}"); await ArduinoDevice.transferPacketAsync(msg, 10); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); TestFixtureCi.ClearLogs(); } // sends C2D - between 10 and 99 var c2dMessageBody = (100 + random.Next(90)).ToString(CultureInfo.InvariantCulture); var msgId = Guid.NewGuid().ToString(); var c2dMessage = new LoRaCloudToDeviceMessage() { Payload = c2dMessageBody, Fport = FramePorts.App1, MessageId = msgId, Confirmed = true, }; await TestFixtureCi.SendCloudToDeviceMessageAsync(device.DeviceID, c2dMessage); Log($"Message {c2dMessageBody} sent to device, need to check if it receives"); var foundC2DMessageCount = 0; var foundReceivePacketCount = 0; var expectedRxSerial = $"+MSG: PORT: 1; RX: \"{ToHexString(c2dMessageBody)}\""; var expectedTcpMessageV1 = $"{device.DevAddr}: ConfirmedDataDown"; var expectedTcpMessageV2 = $"{device.DeviceID}: cloud to device message: {ToHexString(c2dMessageBody)}, id: {msgId}, fport: 1, confirmed: True"; Log($"Expected C2D received log is: {expectedRxSerial}"); Log($"Expected TCP log starting with: {expectedTcpMessageV1} or {expectedTcpMessageV2}"); // Sends 8x unconfirmed messages, stopping if C2D message is found for (var i = warmUpMessageCount + 1; i <= messagesToSend; ++i) { var msg = PayloadGenerator.Next().ToString(CultureInfo.InvariantCulture); Log($"{device.DeviceID}: Sending unconfirmed '{msg}' {i}/{messagesToSend}"); await ArduinoDevice.transferPacketAsync(msg, 10); await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET); await AssertUtils.ContainsWithRetriesAsync("+MSG: Done", ArduinoDevice.SerialLogs); // check if c2d message was found var searchResults = await TestFixtureCi.SearchNetworkServerModuleAsync( (messageBody) => { return(messageBody.StartsWith(expectedTcpMessageV1, StringComparison.OrdinalIgnoreCase) || messageBody.StartsWith(expectedTcpMessageV2, StringComparison.OrdinalIgnoreCase)); }, new SearchLogOptions($"{expectedTcpMessageV1} or {expectedTcpMessageV2}") { MaxAttempts = 1 }); // We should only receive the message once if (searchResults.Found) { foundC2DMessageCount++; Log($"{device.DeviceID}: Found C2D message in log (after sending {i}/{messagesToSend}) {foundC2DMessageCount} times"); EnsureNotSeenTooManyTimes(foundC2DMessageCount); } var localFoundCloudToDeviceInSerial = ArduinoDevice.SerialLogs.Contains(expectedRxSerial); if (localFoundCloudToDeviceInSerial) { foundReceivePacketCount++; Log($"{device.DeviceID}: Found C2D message in serial logs (after sending {i}/{messagesToSend}) {foundReceivePacketCount} times"); EnsureNotSeenTooManyTimes(foundReceivePacketCount); } TestFixtureCi.ClearLogs(); await Task.Delay(Constants.DELAY_BETWEEN_MESSAGES); } Assert.True(foundC2DMessageCount > 0, $"Did not find {expectedTcpMessageV1} or {expectedTcpMessageV2} in logs"); // checks if log arrived if (foundReceivePacketCount == 0) { if (ArduinoDevice.SerialLogs.Contains(expectedRxSerial)) { foundReceivePacketCount++; } } Assert.True(foundReceivePacketCount > 0, $"Could not find lora receiving message '{expectedRxSerial}'"); }