Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<PackageVersion Include="Google.Protobuf" Version="3.30.1" />
<PackageVersion Include="Google.Protobuf.Tools" Version="3.30.1" />
<PackageVersion Include="InTheHand.BluetoothLE" Version="4.0.41" />
<PackageVersion Include="LibUsbDotNet" Version="2.2.75" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.3" />
Expand Down
12 changes: 3 additions & 9 deletions Meshtastic.Cli/DeviceConnectionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@
namespace Meshtastic.Cli;

[ExcludeFromCodeCoverage(Justification = "Requires hardware")]
public class DeviceConnectionContext
public class DeviceConnectionContext(string? port, string? host)
{
public readonly string? Port;
public readonly string? Host;

public DeviceConnectionContext(string? port, string? host)
{
this.Port = port;
this.Host = host;
}
public readonly string? Port = port;
public readonly string? Host = host;

public DeviceConnection GetDeviceConnection(ILogger logger)
{
Expand Down
32 changes: 32 additions & 0 deletions Meshtastic.IntegrationTest/GlobalSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Meshtastic.Discovery;
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;

namespace Meshtastic.IntegrationTest
{
[SetUpFixture]
public class GlobalSetup
{
public static List<MeshtasticDevice> DiscoveredDevices { get; private set; } = new();

[OneTimeSetUp]
public void RunBeforeAnyTests()
{
var discovery = new DeviceDiscovery();
var usbDevices = discovery.DiscoverUsbDevices();

var seeedL1Tracker = usbDevices.FirstOrDefault(d => d.HwModel == HardwareModel.SeeedWioTrackerL1);
if (seeedL1Tracker != null)
DiscoveredDevices.Add(seeedL1Tracker);

var rak4631Device = usbDevices.FirstOrDefault(d => d.HwModel == HardwareModel.Rak4631);
if (rak4631Device != null)
DiscoveredDevices.Add(rak4631Device);

var heltecV3Device = usbDevices.FirstOrDefault(d => d.HwModel == HardwareModel.HeltecV3);
if (heltecV3Device != null)
DiscoveredDevices.Add(heltecV3Device);
}
}
}
25 changes: 25 additions & 0 deletions Meshtastic.IntegrationTest/Meshtastic.IntegrationTest.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="NUnit.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Meshtastic\Meshtastic.csproj" />
</ItemGroup>
</Project>
81 changes: 81 additions & 0 deletions Meshtastic.IntegrationTest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Meshtastic Integration Tests

This project contains integration tests that require actual Meshtastic hardware devices connected via serial port. These tests are designed to verify real-world functionality of the Meshtastic C# library.

## Prerequisites

- A Meshtastic device connected via USB/serial port
- The device should be configured and operational
- .NET 9.0 SDK

## Environment Variables

You can configure the tests using the following environment variables:

- `MESHTASTIC_SERIAL_PORT`: Specify the serial port to use (e.g., `COM3` on Windows, `/dev/ttyUSB0` on Linux, `/dev/cu.usbserial-...` on macOS)
- `MESHTASTIC_DEST_NODE`: Specify a destination node number for targeted messages (optional, defaults to broadcast)

## Running the Tests

### Command Line

```bash
# Run all integration tests
dotnet test Meshtastic.IntegrationTest

# Run with specific serial port
MESHTASTIC_SERIAL_PORT=/dev/ttyUSB0 dotnet test Meshtastic.IntegrationTest

# Run with destination node
MESHTASTIC_SERIAL_PORT=COM3 MESHTASTIC_DEST_NODE=123456789 dotnet test Meshtastic.IntegrationTest

# Run only hardware tests
dotnet test Meshtastic.IntegrationTest --filter Category=RequiresHardware
```

### Visual Studio / VS Code

1. Set the environment variables in your test runner configuration
2. Run tests normally through the test explorer

## Test Categories

The tests are organized using NUnit categories:

- `IntegrationTest`: All integration tests
- `RequiresHardware`: Tests that require actual hardware
- `SerialDevice`: Tests that specifically require serial connection

## Expected Device Configuration

For the tests to work properly, your Meshtastic device should:

1. Be powered on and operational
2. Have a valid configuration (channels, etc.)
3. Be connected via USB/serial
4. Have the primary channel configured for text messaging

## Troubleshooting

### No Serial Ports Found

- Ensure your device is connected and recognized by the OS
- Check that the device is not being used by another application
- Verify the USB/serial drivers are installed

### Test Timeouts

- Check that the device is powered on and responsive
- Verify the device has a valid configuration
- Ensure the destination node (if specified) exists and is reachable

### Permission Issues (Linux/macOS)

You may need to add your user to the appropriate group:

```bash
# Linux
sudo usermod -a -G dialout $USER

# macOS - usually no additional permissions needed
```
7 changes: 7 additions & 0 deletions Meshtastic.IntegrationTest/TestCategories.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Meshtastic.IntegrationTest;

public static class TestCategories
{
public const string RequiresHardware = "RequiresHardware";
public const string SerialDevice = "SerialDevice";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using Meshtastic.Discovery;

namespace Meshtastic.IntegrationTest.Tests.ConnectedDeviceTests;

[TestFixture]
[Category(TestCategories.RequiresHardware)]
[Category(TestCategories.SerialDevice)]
public class TextMessageTests : IntegrationTestBase
{
private ILogger _logger = null!;

[OneTimeSetUp]
public void OneTimeSetUp()
{
// Set up logging
using var loggerFactory = LoggerFactory.Create(builder =>
builder.AddConsole()
.SetMinimumLevel(LogLevel.Debug));
_logger = loggerFactory.CreateLogger<TextMessageTests>();
}

[Test]
[CancelAfter(15000)] // 15 second timeout
[TestCaseSource(nameof(GetDevicesUnderTest))]
public async Task SendTextMessage_ShouldReceiveAck_WhenDeviceConnected(MeshtasticDevice device)
{
var testMessage = $"Integration test message at {DateTime.Now:HH:mm:ss}";
var ackReceived = false;
var errorReason = Routing.Types.Error.None;

_logger.LogInformation($"Testing device: {device.Name} on {device.SerialPort}");

var connection = new SerialConnection(_logger, device.SerialPort!);

try
{
// First, get device configuration to establish connection
var wantConfig = new ToRadioMessageFactory().CreateWantConfigMessage();
var container = await connection.WriteToRadio(wantConfig, async (fromRadio, container) =>
{
// Complete when we have MyNodeInfo (minimum needed for sending messages)
return await Task.FromResult(container.MyNodeInfo.MyNodeNum != 0);
});

container.Should().NotBeNull();
container.MyNodeInfo.Should().NotBeNull();
container.MyNodeInfo.MyNodeNum.Should().NotBe(0u);

_logger.LogInformation($"Connected to device. Node number: {container.MyNodeInfo.MyNodeNum}");

// Create and send text message
var textMessageFactory = new TextMessageFactory(container);
var textMessage = textMessageFactory.CreateTextMessagePacket(testMessage);

_logger.LogInformation($"Sending text message: '{testMessage}'");

// Send message and wait for ACK
var toRadioFactory = new ToRadioMessageFactory();
await connection.WriteToRadio(toRadioFactory.CreateMeshPacketMessage(textMessage),
async (fromRadio, container) =>
{
var routingResult = fromRadio.GetPayload<Routing>();
if (routingResult != null && fromRadio.Packet?.Priority == MeshPacket.Types.Priority.Ack)
{
errorReason = routingResult.ErrorReason;
ackReceived = true;

if (routingResult.ErrorReason == Routing.Types.Error.None)
_logger.LogInformation("✅ Message acknowledged successfully");
else
_logger.LogWarning($"❌ Message delivery failed: {routingResult.ErrorReason}");

return await Task.FromResult(true);
}

// Log other incoming messages for debugging
if (fromRadio.Packet != null)
{
_logger.LogDebug($"Received packet: {fromRadio.Packet.Decoded?.Portnum} Priority: {fromRadio.Packet.Priority}");
}

return await Task.FromResult(false);
});
}
finally
{
connection.Disconnect();
}

// Assert
ackReceived.Should().BeTrue("An ACK should be received for the sent message");
errorReason.Should().Be(Routing.Types.Error.None, "The message should be delivered without errors");
}

[Test]
[CancelAfter(15000)] // 15 second timeout
[TestCaseSource(nameof(GetDevicesUnderTest))]
public async Task GetDeviceInfo_ShouldReturnValidNodeInfo(MeshtasticDevice device)
{
// Arrange & Act
_logger.LogInformation($"Testing device info for: {device.Name} on {device.SerialPort}");

var connection = new SerialConnection(_logger, device.SerialPort!);

DeviceStateContainer? container = null;

try
{
var toRadioFactory = new ToRadioMessageFactory();
var wantConfig = toRadioFactory.CreateWantConfigMessage();
container = await connection.WriteToRadio(wantConfig, async (fromRadio, container) =>
{
// Complete when we have sufficient device info
var deviceNode = container.GetDeviceNodeInfo();
return await Task.FromResult(
container.MyNodeInfo.MyNodeNum != 0 &&
deviceNode != null &&
!string.IsNullOrEmpty(deviceNode.User?.LongName));
});
}
finally
{
connection.Disconnect();
}

// Assert
container.Should().NotBeNull();
container!.MyNodeInfo.Should().NotBeNull();
container.MyNodeInfo.MyNodeNum.Should().NotBe(0u);

var deviceNode = container.GetDeviceNodeInfo();
deviceNode.Should().NotBeNull();
deviceNode!.User.Should().NotBeNull();
deviceNode.User!.LongName.Should().NotBeNullOrEmpty();

_logger.LogInformation($"Device Info - Node: {container.MyNodeInfo.MyNodeNum}, " +
$"Name: '{deviceNode.User.LongName}', " +
$"Short: '{deviceNode.User.ShortName}'");
}
}
8 changes: 8 additions & 0 deletions Meshtastic.IntegrationTest/Tests/IntegrationTestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Meshtastic.Discovery;

namespace Meshtastic.IntegrationTest.Tests;

public class IntegrationTestBase
{
protected static IEnumerable<MeshtasticDevice> GetDevicesUnderTest() => GlobalSetup.DiscoveredDevices;
}
8 changes: 8 additions & 0 deletions Meshtastic.IntegrationTest/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
global using NUnit.Framework;
global using FluentAssertions;
global using Microsoft.Extensions.Logging;
global using Meshtastic.Connections;
global using Meshtastic.Data;
global using Meshtastic.Data.MessageFactories;
global using Meshtastic.Protobufs;
global using Meshtastic.Extensions;
Loading
Loading