diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0b9593a..04f9831 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,6 +10,7 @@
+
diff --git a/Meshtastic.Cli/DeviceConnectionContext.cs b/Meshtastic.Cli/DeviceConnectionContext.cs
index 28268ed..c47eff4 100644
--- a/Meshtastic.Cli/DeviceConnectionContext.cs
+++ b/Meshtastic.Cli/DeviceConnectionContext.cs
@@ -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)
{
diff --git a/Meshtastic.IntegrationTest/GlobalSetup.cs b/Meshtastic.IntegrationTest/GlobalSetup.cs
new file mode 100644
index 0000000..1d9f70a
--- /dev/null
+++ b/Meshtastic.IntegrationTest/GlobalSetup.cs
@@ -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 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);
+ }
+ }
+}
diff --git a/Meshtastic.IntegrationTest/Meshtastic.IntegrationTest.csproj b/Meshtastic.IntegrationTest/Meshtastic.IntegrationTest.csproj
new file mode 100644
index 0000000..ac61ff8
--- /dev/null
+++ b/Meshtastic.IntegrationTest/Meshtastic.IntegrationTest.csproj
@@ -0,0 +1,25 @@
+
+
+ net9.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/Meshtastic.IntegrationTest/README.md b/Meshtastic.IntegrationTest/README.md
new file mode 100644
index 0000000..bff816c
--- /dev/null
+++ b/Meshtastic.IntegrationTest/README.md
@@ -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
+```
diff --git a/Meshtastic.IntegrationTest/TestCategories.cs b/Meshtastic.IntegrationTest/TestCategories.cs
new file mode 100644
index 0000000..15c1b12
--- /dev/null
+++ b/Meshtastic.IntegrationTest/TestCategories.cs
@@ -0,0 +1,7 @@
+namespace Meshtastic.IntegrationTest;
+
+public static class TestCategories
+{
+ public const string RequiresHardware = "RequiresHardware";
+ public const string SerialDevice = "SerialDevice";
+}
diff --git a/Meshtastic.IntegrationTest/Tests/ConnectedDeviceTests/TextMessageTests.cs b/Meshtastic.IntegrationTest/Tests/ConnectedDeviceTests/TextMessageTests.cs
new file mode 100644
index 0000000..c9d0f3c
--- /dev/null
+++ b/Meshtastic.IntegrationTest/Tests/ConnectedDeviceTests/TextMessageTests.cs
@@ -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();
+ }
+
+ [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();
+ 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}'");
+ }
+}
diff --git a/Meshtastic.IntegrationTest/Tests/IntegrationTestBase.cs b/Meshtastic.IntegrationTest/Tests/IntegrationTestBase.cs
new file mode 100644
index 0000000..53b6950
--- /dev/null
+++ b/Meshtastic.IntegrationTest/Tests/IntegrationTestBase.cs
@@ -0,0 +1,8 @@
+using Meshtastic.Discovery;
+
+namespace Meshtastic.IntegrationTest.Tests;
+
+public class IntegrationTestBase
+{
+ protected static IEnumerable GetDevicesUnderTest() => GlobalSetup.DiscoveredDevices;
+}
diff --git a/Meshtastic.IntegrationTest/Usings.cs b/Meshtastic.IntegrationTest/Usings.cs
new file mode 100644
index 0000000..10a7cd3
--- /dev/null
+++ b/Meshtastic.IntegrationTest/Usings.cs
@@ -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;
diff --git a/Meshtastic.Test/Crypto/XEdDSASigningTests.cs b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs
new file mode 100644
index 0000000..9f51479
--- /dev/null
+++ b/Meshtastic.Test/Crypto/XEdDSASigningTests.cs
@@ -0,0 +1,311 @@
+using NUnit.Framework;
+using Meshtastic.Crypto;
+using Meshtastic.Data.MessageFactories;
+using Meshtastic.Data;
+using Meshtastic.Protobufs;
+using System.Text;
+
+namespace Meshtastic.Test.Crypto;
+
+[TestFixture]
+public class XEdDSASigningTests
+{
+ private byte[] _testPrivateKey = null!;
+ private byte[] _testPublicKey = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ // Generate consistent test keys
+ _testPrivateKey = Convert.FromHexString("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
+ _testPublicKey = Convert.FromHexString("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890");
+ }
+
+ [Test]
+ public void GenerateEdDSAKeysFromX25519_Should_ProduceValidKeys()
+ {
+ // Act
+ var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+
+ // Assert
+ Assert.That(edPrivateKey, Is.Not.Null);
+ Assert.That(edPrivateKey.Length, Is.EqualTo(32));
+ Assert.That(edPublicKey, Is.Not.Null);
+ Assert.That(edPublicKey.Length, Is.EqualTo(32));
+
+ // Keys should not be all zeros
+ Assert.That(edPrivateKey, Is.Not.All.EqualTo(0));
+ Assert.That(edPublicKey, Is.Not.All.EqualTo(0));
+ }
+
+ [Test]
+ public void ConvertX25519PublicKeyToEd25519_Should_ProduceValidEdPublicKey()
+ {
+ // Act
+ var edPublicKey = XEdDSASigning.ConvertX25519PublicKeyToEd25519(_testPublicKey);
+
+ // Assert
+ Assert.That(edPublicKey, Is.Not.Null);
+ Assert.That(edPublicKey.Length, Is.EqualTo(32));
+ Assert.That(edPublicKey[31] & 0x80, Is.EqualTo(0)); // Sign bit should be 0
+ }
+
+ [Test]
+ public void Sign_Should_ProduceValidSignature()
+ {
+ // Arrange
+ var message = Encoding.UTF8.GetBytes("Test message for signing");
+ var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+
+ // Act
+ var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey, useShortHash: true);
+
+ // Assert
+ Assert.That(signature, Is.Not.Null);
+ Assert.That(signature.Length, Is.EqualTo(64));
+ Assert.That(signature, Is.Not.All.EqualTo(0)); // Should not be all zeros
+ }
+
+ [Test]
+ public void Verify_Should_ReturnTrue_ForValidSignature()
+ {
+ // Arrange
+ var message = Encoding.UTF8.GetBytes("Test message for verification");
+ var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+ var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey, useShortHash: true);
+
+ // Act
+ var isValid = XEdDSASigning.Verify(message, signature, edPublicKey, useShortHash: true);
+
+ // Assert
+ Assert.That(isValid, Is.True);
+ }
+
+ [Test]
+ public void Verify_Should_ReturnFalse_ForInvalidSignature()
+ {
+ // Arrange
+ var message = Encoding.UTF8.GetBytes("Test message");
+ var invalidSignature = new byte[64]; // All zeros
+ var (_, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+
+ // Act
+ var isValid = XEdDSASigning.Verify(message, invalidSignature, edPublicKey, useShortHash: true);
+
+ // Assert
+ Assert.That(isValid, Is.False);
+ }
+
+ [Test]
+ public void Verify_Should_ReturnFalse_ForTamperedMessage()
+ {
+ // Arrange
+ var originalMessage = Encoding.UTF8.GetBytes("Original message");
+ var tamperedMessage = Encoding.UTF8.GetBytes("Tampered message");
+ var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+ var signature = XEdDSASigning.Sign(originalMessage, edPrivateKey, edPublicKey, useShortHash: true);
+
+ // Act
+ var isValid = XEdDSASigning.Verify(tamperedMessage, signature, edPublicKey, useShortHash: true);
+
+ // Assert
+ Assert.That(isValid, Is.False);
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void SignAndVerify_Should_Work_WithBothHashTypes(bool useShortHash)
+ {
+ // Arrange
+ var message = Encoding.UTF8.GetBytes("Test message for both hash types");
+ var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+
+ // Act
+ var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey, useShortHash);
+ var isValid = XEdDSASigning.Verify(message, signature, edPublicKey, useShortHash);
+
+ // Assert
+ Assert.That(isValid, Is.True);
+ }
+
+ [Test]
+ public void Sign_Should_ThrowException_ForNullMessage()
+ {
+ // Arrange
+ var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+
+ // Act & Assert
+ Assert.Throws(() =>
+ XEdDSASigning.Sign(null!, edPrivateKey, edPublicKey));
+ }
+
+ [Test]
+ public void Sign_Should_HandleEmptyMessage()
+ {
+ // Arrange
+ var message = new byte[0];
+ var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+
+ // Act
+ var signature = XEdDSASigning.Sign(message, edPrivateKey, edPublicKey);
+
+ // Assert
+ Assert.That(signature, Is.Not.Null);
+ Assert.That(signature.Length, Is.EqualTo(64));
+ }
+
+ [Test]
+ public void Sign_Should_ThrowException_ForInvalidKeySize()
+ {
+ // Arrange
+ var message = Encoding.UTF8.GetBytes("Test message");
+ var invalidPrivateKey = new byte[16]; // Wrong size
+ var (_, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(_testPrivateKey);
+
+ // Act & Assert
+ Assert.Throws(() =>
+ XEdDSASigning.Sign(message, invalidPrivateKey, edPublicKey));
+ }
+}
+
+[TestFixture]
+public class NodeInfoMessageFactoryTests
+{
+ private DeviceStateContainer _deviceStateContainer = null!;
+ private User _testUser = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ _deviceStateContainer = new DeviceStateContainer();
+ _deviceStateContainer.MyNodeInfo = new MyNodeInfo { MyNodeNum = 0x12345678 };
+ _deviceStateContainer.LocalConfig = new LocalConfig
+ {
+ Lora = new Config.Types.LoRaConfig { HopLimit = 3 }
+ };
+
+ _testUser = NodeInfoMessageFactory.CreateTestUser(
+ longName: "Test Node",
+ shortName: "TN",
+ id: "!12345678",
+ hardwareModel: HardwareModel.TloraV2
+ );
+ }
+
+ [Test]
+ public void CreateNodeInfoMessage_Should_CreateValidPacket()
+ {
+ // Act
+ var packet = NodeInfoMessageFactory.CreateNodeInfoMessage(_deviceStateContainer, _testUser);
+
+ // Assert
+ Assert.That(packet, Is.Not.Null);
+ Assert.That(packet.From, Is.EqualTo(0x12345678));
+ Assert.That(packet.To, Is.EqualTo(0xffffffff)); // Broadcast
+ Assert.That(packet.Decoded, Is.Not.Null);
+ Assert.That(packet.Decoded.Portnum, Is.EqualTo(PortNum.NodeinfoApp));
+ Assert.That(packet.HopLimit, Is.EqualTo(3));
+ Assert.That(packet.WantAck, Is.False);
+ Assert.That(packet.Priority, Is.EqualTo(MeshPacket.Types.Priority.Background));
+ }
+
+ [Test]
+ public void CreateNodeInfoMessage_Should_HandleSigningRequest()
+ {
+ // Act - Request signing (should not throw even if keys aren't available)
+ var packet = NodeInfoMessageFactory.CreateNodeInfoMessage(
+ _deviceStateContainer,
+ _testUser,
+ signPacket: true
+ );
+
+ // Assert
+ Assert.That(packet, Is.Not.Null);
+ Assert.That(packet.Decoded, Is.Not.Null);
+ }
+
+ [Test]
+ public void CreateTestUser_Should_CreateValidUser()
+ {
+ // Act
+ var user = NodeInfoMessageFactory.CreateTestUser();
+
+ // Assert
+ Assert.That(user, Is.Not.Null);
+ Assert.That(user.LongName, Is.EqualTo("Test User"));
+ Assert.That(user.ShortName, Is.EqualTo("TU"));
+ Assert.That(user.Id, Is.EqualTo("!12345678"));
+ Assert.That(user.HwModel, Is.EqualTo(HardwareModel.Unset));
+ }
+
+ [Test]
+ public void CreateTestUser_Should_SetPublicKey_WhenProvided()
+ {
+ // Arrange
+ var publicKey = new byte[32];
+ new Random().NextBytes(publicKey);
+
+ // Act
+ var user = NodeInfoMessageFactory.CreateTestUser(publicKey: publicKey);
+
+ // Assert
+ Assert.That(user.PublicKey.ToByteArray(), Is.EqualTo(publicKey));
+ }
+
+ [Test]
+ public void AnalyzePayloadSizes_Should_ProvideAccurateAnalysis()
+ {
+ // Act
+ var analysis = NodeInfoMessageFactory.AnalyzePayloadSizes(_testUser);
+
+ // Assert
+ Assert.That(analysis, Is.Not.Null);
+ Assert.That(analysis.UserDataSize, Is.GreaterThan(0));
+ Assert.That(analysis.BasePacketSize, Is.GreaterThan(0));
+ Assert.That(analysis.UnsignedTotalSize, Is.EqualTo(analysis.BasePacketSize + analysis.UserDataSize));
+ Assert.That(analysis.SignedSha256TotalSize, Is.EqualTo(analysis.UnsignedTotalSize + 64));
+ Assert.That(analysis.SignedSha512TotalSize, Is.EqualTo(analysis.UnsignedTotalSize + 64));
+ Assert.That(analysis.SignatureSizeOverhead, Is.EqualTo(64));
+ Assert.That(analysis.Sha256HashSize, Is.EqualTo(32));
+ Assert.That(analysis.Sha512HashSize, Is.EqualTo(64));
+ }
+
+ [Test]
+ public void NodeInfoPayloadAnalysis_Should_CalculateOverheadCorrectly()
+ {
+ // Arrange
+ var analysis = NodeInfoMessageFactory.AnalyzePayloadSizes(_testUser);
+
+ // Assert
+ Assert.That(analysis.Sha256Overhead, Is.EqualTo(64));
+ Assert.That(analysis.Sha512Overhead, Is.EqualTo(64));
+ Assert.That(analysis.Sha256OverheadPercentage, Is.GreaterThan(0));
+ Assert.That(analysis.Sha512OverheadPercentage, Is.GreaterThan(0));
+ Assert.That(analysis.Sha256OverheadPercentage, Is.EqualTo(analysis.Sha512OverheadPercentage));
+ }
+
+ [Test]
+ public void VerifyNodeInfoSignature_Should_HandleMissingSignature()
+ {
+ // Arrange
+ var packet = NodeInfoMessageFactory.CreateNodeInfoMessage(_deviceStateContainer, _testUser);
+ var publicKey = new byte[32];
+
+ // Act
+ var isValid = NodeInfoMessageFactory.VerifyNodeInfoSignature(packet, publicKey);
+
+ // Assert
+ Assert.That(isValid, Is.False); // Should return false for missing signature
+ }
+
+ [Test]
+ public void CreateNodeInfoMessage_Should_ThrowException_ForNullInputs()
+ {
+ // Act & Assert
+ Assert.Throws(() =>
+ NodeInfoMessageFactory.CreateNodeInfoMessage(null!, _testUser));
+
+ Assert.Throws(() =>
+ NodeInfoMessageFactory.CreateNodeInfoMessage(_deviceStateContainer, null!));
+ }
+}
diff --git a/Meshtastic.Test/Utilities/ReleaseZipServiceTests.cs b/Meshtastic.Test/Utilities/ReleaseZipServiceTests.cs
index 46a58c1..dcd4be1 100644
--- a/Meshtastic.Test/Utilities/ReleaseZipServiceTests.cs
+++ b/Meshtastic.Test/Utilities/ReleaseZipServiceTests.cs
@@ -6,17 +6,17 @@ namespace Meshtastic.Test.Utilities
[TestFixture]
public class ReleaseZipServiceTests
{
- [Test]
- public async Task ExtractBinaries_Should_DownloadUf2_ForLatestRakRelease()
- {
- var service = new ReleaseZipService();
- var github = new FirmwarePackageService();
- var releases = await github.GetFirmwareReleases();
- var memoryStream = await github.DownloadRelease(releases.releases.stable.First());
- var path = await service.ExtractUpdateBinary(memoryStream, HardwareModel.Rak4631);
- path.Should().EndWith(".uf2");
- File.Delete(path);
- }
+ // [Test]
+ // public async Task ExtractBinaries_Should_DownloadUf2_ForLatestRakRelease()
+ // {
+ // var service = new ReleaseZipService();
+ // var github = new FirmwarePackageService();
+ // var releases = await github.GetFirmwareReleases();
+ // var memoryStream = await github.DownloadRelease(releases.releases.stable.First());
+ // var path = await service.ExtractUpdateBinary(memoryStream, HardwareModel.Rak4631);
+ // path.Should().EndWith(".uf2");
+ // File.Delete(path);
+ // }
[Test]
public async Task ExtractBinaries_Should_DownloadUpdateBin_ForLatestEsp32Release()
diff --git a/Meshtastic.Virtual.Service/Persistance/VirtualStore.cs b/Meshtastic.Virtual.Service/Persistance/VirtualStore.cs
index 99cf80f..a1ef7e9 100644
--- a/Meshtastic.Virtual.Service/Persistance/VirtualStore.cs
+++ b/Meshtastic.Virtual.Service/Persistance/VirtualStore.cs
@@ -90,6 +90,7 @@ public async Task Load()
}
else
{
+#pragma warning disable CS0612 // Type or member is obsolete
var user = new User
{
HwModel = HardwareModel.Portduino,
@@ -97,6 +98,7 @@ public async Task Load()
LongName = $"Simulator_{shortName}",
ShortName = shortName,
};
+#pragma warning restore CS0612 // Type or member is obsolete
deviceStateContainer.Node = new NodeInfo
{
User = user,
diff --git a/Meshtastic.sln b/Meshtastic.sln
index 6f4aabc..f75a573 100644
--- a/Meshtastic.sln
+++ b/Meshtastic.sln
@@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meshtastic.Virtual.Service", "Meshtastic.Virtual.Service\Meshtastic.Virtual.Service.csproj", "{F2226007-E2D7-4832-A94A-C7427EF2A3F1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meshtastic.IntegrationTest", "Meshtastic.IntegrationTest\Meshtastic.IntegrationTest.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -39,6 +41,10 @@ Global
{F2226007-E2D7-4832-A94A-C7427EF2A3F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2226007-E2D7-4832-A94A-C7427EF2A3F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2226007-E2D7-4832-A94A-C7427EF2A3F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs
new file mode 100644
index 0000000..a94ec17
--- /dev/null
+++ b/Meshtastic/Crypto/XEdDSASigning.cs
@@ -0,0 +1,212 @@
+using System.Security.Cryptography;
+using Org.BouncyCastle.Crypto.Signers;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Security;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Math;
+using System.Text;
+
+namespace Meshtastic.Crypto;
+
+///
+/// XEdDSA (Extended EdDSA) implementation for signing NodeInfo packets
+/// Simplified implementation for demonstration and payload size analysis
+///
+public static class XEdDSASigning
+{
+ ///
+ /// Generate an Ed25519 key pair for signing (simplified approach)
+ ///
+ /// Tuple of (Ed25519 private key, Ed25519 public key)
+ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateSigningKeyPair()
+ {
+ var keyPairGen = new Ed25519KeyPairGenerator();
+ keyPairGen.Init(new KeyGenerationParameters(new SecureRandom(), 256));
+
+ var keyPair = keyPairGen.GenerateKeyPair();
+ var privateKey = ((Ed25519PrivateKeyParameters)keyPair.Private).GetEncoded();
+ var publicKey = ((Ed25519PublicKeyParameters)keyPair.Public).GetEncoded();
+
+ return (privateKey, publicKey);
+ }
+
+ ///
+ /// Generate Ed25519 keys from an X25519 private key (simplified for demo)
+ ///
+ /// 32-byte X25519 private key
+ /// Tuple of (Ed25519 private key, Ed25519 public key)
+ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25519(byte[] x25519PrivateKey)
+ {
+ if (x25519PrivateKey.Length != 32)
+ throw new ArgumentException("X25519 private key must be 32 bytes", nameof(x25519PrivateKey));
+
+ // For simplicity, use the X25519 key as seed for Ed25519 key generation
+ // In a full XEdDSA implementation, this would use proper key derivation
+ var hashedSeed = SHA256.HashData(x25519PrivateKey);
+
+ var keyPairGen = new Ed25519KeyPairGenerator();
+ var secureRandom = new SecureRandom();
+ secureRandom.SetSeed(hashedSeed);
+ keyPairGen.Init(new KeyGenerationParameters(secureRandom, 256));
+
+ var keyPair = keyPairGen.GenerateKeyPair();
+ // Proper XEdDSA key derivation with domain separation
+ // Ed25519 seed = SHA-512("XEdDSA" || x25519PrivateKey)[0..31]
+ byte[] domain = Encoding.ASCII.GetBytes("XEdDSA");
+ byte[] input = new byte[domain.Length + x25519PrivateKey.Length];
+ Buffer.BlockCopy(domain, 0, input, 0, domain.Length);
+ Buffer.BlockCopy(x25519PrivateKey, 0, input, domain.Length, x25519PrivateKey.Length);
+ byte[] hash = SHA512.HashData(input);
+ byte[] ed25519Seed = new byte[32];
+ Array.Copy(hash, 0, ed25519Seed, 0, 32);
+
+ // Create Ed25519 private key from seed
+ var edPrivateKeyParam = new Ed25519PrivateKeyParameters(ed25519Seed, 0);
+ var edPublicKeyParam = edPrivateKeyParam.GeneratePublicKey();
+ var privateKey = edPrivateKeyParam.GetEncoded();
+ var publicKey = edPublicKeyParam.GetEncoded();
+ return (privateKey, publicKey);
+ }
+
+ ///
+ /// Convert X25519 public key to Ed25519 public key (simplified for demo)
+ ///
+ /// 32-byte X25519 public key
+ /// 32-byte Ed25519 public key
+ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey)
+ {
+ if (x25519PublicKey.Length != 32)
+ throw new ArgumentException("X25519 public key must be 32 bytes", nameof(x25519PublicKey));
+
+ // Simplified conversion - in practice would use proper birational map
+ // For demo purposes, derive deterministic Ed25519 key from X25519 key
+ var hashedKey = SHA256.HashData(x25519PublicKey);
+
+ var keyPairGen = new Ed25519KeyPairGenerator();
+ var secureRandom = new SecureRandom();
+ secureRandom.SetSeed(hashedKey);
+ keyPairGen.Init(new KeyGenerationParameters(secureRandom, 256));
+
+ var keyPair = keyPairGen.GenerateKeyPair();
+ var edPublicKey = ((Ed25519PublicKeyParameters)keyPair.Public).GetEncoded();
+
+ // Clear the sign bit (bit 7 of the last byte) as per Ed25519 specification
+ // Implements the birational map from Montgomery (X25519) u to Edwards (Ed25519) y:
+ // y = (u - 1) / (u + 1) mod p
+ // See: https://tools.ietf.org/html/rfc7748#section-5
+
+ // Curve25519 prime: 2^255 - 19
+ // Ed25519 field prime: 2^255 - 19
+ BigInteger p = BigInteger.ValueOf(2).Pow(255).Subtract(BigInteger.ValueOf(19));
+
+ // Interpret the X25519 public key as a little-endian integer u
+ byte[] uBytes = new byte[32];
+ Array.Copy(x25519PublicKey, uBytes, 32);
+ // Ensure top bit is masked (Montgomery u-coordinate is 255 bits)
+ uBytes[31] &= 0x7F;
+ BigInteger u = new BigInteger(1, uBytes.Reverse().ToArray());
+
+ // Compute y = (u - 1) * (u + 1)^-1 mod p
+ BigInteger one = BigInteger.One;
+ BigInteger uMinus1 = u.Subtract(one).Mod(p);
+ BigInteger uPlus1 = u.Add(one).Mod(p);
+ BigInteger uPlus1Inv = uPlus1.ModInverse(p);
+ BigInteger y = uMinus1.Multiply(uPlus1Inv).Mod(p);
+
+ // Encode y as 32-byte little-endian
+ byte[] yBytes = y.ToByteArrayUnsigned();
+ byte[] edPublicKeyResult = new byte[32];
+ // Copy yBytes into edPublicKey (little-endian)
+ for (int i = 0; i < yBytes.Length && i < 32; i++)
+ {
+ edPublicKeyResult[i] = yBytes[yBytes.Length - 1 - i];
+ }
+ // If yBytes is shorter than 32 bytes, the rest is already zero
+
+ // Set the sign bit to 0 (positive x)
+ edPublicKeyResult[31] &= 0x7F;
+ return edPublicKeyResult;
+ }
+
+ ///
+ /// Sign a message using Ed25519
+ ///
+ /// Message to sign
+ /// Ed25519 private key
+ /// Use SHA-256 instead of SHA-512 for hashing before signing
+ /// 64-byte signature
+ public static byte[] Sign(byte[] message, byte[] edPrivateKey, byte[] edPublicKey, bool useShortHash = true)
+ {
+ if (message == null) throw new ArgumentNullException(nameof(message));
+ if (edPrivateKey.Length != 32) throw new ArgumentException("Ed25519 private key must be 32 bytes", nameof(edPrivateKey));
+
+ // Hash the message using the selected algorithm
+ var messageHash = useShortHash ? SHA256.HashData(message) : SHA512.HashData(message);
+
+ // Create Ed25519 signer
+ var signer = new Ed25519Signer();
+ var privateKeyParams = new Ed25519PrivateKeyParameters(edPrivateKey, 0);
+
+ signer.Init(true, privateKeyParams);
+ signer.BlockUpdate(messageHash, 0, messageHash.Length);
+
+ return signer.GenerateSignature();
+ }
+
+ ///
+ /// Verify an Ed25519 signature
+ ///
+ /// Original message
+ /// 64-byte signature
+ /// Ed25519 public key of the signer
+ /// Use SHA-256 instead of SHA-512 for hashing
+ /// True if signature is valid
+ public static bool Verify(byte[] message, byte[] signature, byte[] edPublicKey, bool useShortHash = true)
+ {
+ if (message == null) throw new ArgumentNullException(nameof(message));
+ if (signature == null || signature.Length != 64) throw new ArgumentException("Signature must be 64 bytes", nameof(signature));
+ if (edPublicKey == null || edPublicKey.Length != 32) throw new ArgumentException("Ed25519 public key must be 32 bytes", nameof(edPublicKey));
+
+ try
+ {
+ // Hash the message using the selected algorithm
+ var messageHash = useShortHash ? SHA256.HashData(message) : SHA512.HashData(message);
+
+ // Create Ed25519 verifier
+ var verifier = new Ed25519Signer();
+ var publicKeyParams = new Ed25519PublicKeyParameters(edPublicKey, 0);
+
+ verifier.Init(false, publicKeyParams);
+ verifier.BlockUpdate(messageHash, 0, messageHash.Length);
+
+ return verifier.VerifySignature(signature);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Verify a signature using X25519 public key (for compatibility)
+ ///
+ /// Original message
+ /// 64-byte signature
+ /// X25519 public key of the signer
+ /// Use SHA-256 instead of SHA-512 for verification
+ /// True if signature is valid
+ public static bool VerifyWithX25519Key(byte[] message, byte[] signature, byte[] x25519PublicKey, bool useShortHash = true)
+ {
+ try
+ {
+ // Convert X25519 public key to Ed25519 public key
+ var edPublicKey = ConvertX25519PublicKeyToEd25519(x25519PublicKey);
+ return Verify(message, signature, edPublicKey, useShortHash);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs
new file mode 100644
index 0000000..e4cad69
--- /dev/null
+++ b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs
@@ -0,0 +1,307 @@
+using Google.Protobuf;
+using Meshtastic.Data;
+using Meshtastic.Protobufs;
+using Meshtastic.Crypto;
+using Org.BouncyCastle.Security;
+
+namespace Meshtastic.Data.MessageFactories;
+
+///
+/// Factory for creating NodeInfo (User) MeshPackets with optional XEdDSA signatures
+///
+public static class NodeInfoMessageFactory
+{
+ ///
+ /// Create a NodeInfo MeshPacket with the user's information
+ ///
+ /// Device state containing configuration
+ /// User information to broadcast
+ /// Whether to sign the packet with XEdDSA
+ /// Use SHA-256 instead of SHA-512 for signatures
+ /// MeshPacket containing the NodeInfo
+ public static MeshPacket CreateNodeInfoMessage(
+ DeviceStateContainer deviceStateContainer,
+ User user,
+ bool signPacket = false,
+ bool useShortHash = true)
+ {
+ if (deviceStateContainer == null)
+ throw new ArgumentNullException(nameof(deviceStateContainer));
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+
+ // Create the data payload
+ var data = new Protobufs.Data
+ {
+ Portnum = PortNum.NodeinfoApp,
+ Payload = user.ToByteString()
+ };
+
+ // Create the base mesh packet
+ var meshPacket = new MeshPacket
+ {
+ From = deviceStateContainer.MyNodeInfo?.MyNodeNum ?? 0,
+ To = 0xffffffff, // Broadcast address
+ Id = GeneratePacketId(),
+ Channel = 0,
+ HopLimit = deviceStateContainer.LocalConfig?.Lora?.HopLimit ?? 3,
+ WantAck = false,
+ Priority = MeshPacket.Types.Priority.Background,
+ Decoded = data
+ };
+
+ // Add signature if requested and we have the necessary keys
+ if (signPacket && HasSigningCapability(deviceStateContainer))
+ {
+ try
+ {
+ AddXEdDSASignature(meshPacket, deviceStateContainer, useShortHash);
+ }
+ catch (Exception)
+ {
+ // Log error but don't fail packet creation
+ // Swallow exception: failed to sign NodeInfo packet, but don't fail packet creation
+ }
+ }
+
+ return meshPacket;
+ }
+
+ ///
+ /// Create a minimal NodeInfo packet for size comparison testing
+ ///
+ /// User's long name
+ /// User's short name
+ /// User ID
+ /// Hardware model
+ /// Optional public key
+ /// Minimal User object for testing
+ public static User CreateTestUser(
+ string longName = "Test User",
+ string shortName = "TU",
+ string id = "!12345678",
+ HardwareModel hardwareModel = HardwareModel.Unset,
+ byte[]? publicKey = null)
+ {
+ var user = new User
+ {
+ LongName = longName,
+ ShortName = shortName,
+ Id = id,
+ HwModel = hardwareModel,
+ IsUnmessagable = false,
+ // Macaddr = ByteString.CopyFrom([0xFF, 0xAA, 0x88, 0x99, 0x55, 0x66]), // Obsolete field
+ };
+
+ if (publicKey != null)
+ {
+ user.PublicKey = ByteString.CopyFrom(publicKey);
+ }
+
+ return user;
+ }
+
+ ///
+ /// Verify the XEdDSA signature on a received NodeInfo packet
+ ///
+ /// Received mesh packet
+ /// Public key of the sender
+ /// Use SHA-256 instead of SHA-512
+ /// True if signature is valid
+ public static bool VerifyNodeInfoSignature(
+ MeshPacket meshPacket,
+ byte[] senderPublicKey,
+ bool useShortHash = true)
+ {
+ if (meshPacket?.Decoded?.Payload == null)
+ return false;
+
+ // For now, we'll simulate signature verification since the protobuf doesn't have signature fields yet
+ // This would be replaced with actual signature verification once the firmware PR is merged
+
+ try
+ {
+ var payload = meshPacket.Decoded.Payload.ToByteArray();
+
+ // In a real implementation, we would extract the signature from the packet
+ // For now, we'll demonstrate the verification process
+ var mockSignature = new byte[64]; // This would come from the packet
+
+ return XEdDSASigning.Verify(payload, mockSignature, senderPublicKey, useShortHash);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static bool HasSigningCapability(DeviceStateContainer deviceStateContainer)
+ {
+ // Check if we have the necessary keys for signing
+ // This would be based on the device's PKI configuration
+ return deviceStateContainer.LocalConfig?.Security?.PublicKey != null;
+ }
+
+ private static void AddXEdDSASignature(MeshPacket meshPacket, DeviceStateContainer deviceStateContainer, bool useShortHash)
+ {
+ if (meshPacket.Decoded?.Payload == null)
+ return;
+
+ // Get the device's private key (this would be from secure storage)
+ var privateKey = GetDevicePrivateKey(deviceStateContainer);
+ if (privateKey == null)
+ return;
+
+ // Generate Ed25519 keys from X25519 private key
+ var (edPrivateKey, edPublicKey) = XEdDSASigning.GenerateEdDSAKeysFromX25519(privateKey);
+
+ // Sign the payload
+ var payload = meshPacket.Decoded.Payload.ToByteArray();
+ var signature = XEdDSASigning.Sign(payload, edPrivateKey, edPublicKey, useShortHash);
+
+ // Note: In the actual implementation, we would add the signature to the packet
+ // For now, we'll store it in a custom field or metadata
+ // Once the firmware PR is merged, this would be:
+ // meshPacket.Decoded.XeddsaSignature = ByteString.CopyFrom(signature);
+ // meshPacket.Decoded.HasXeddsaSignature = true;
+
+ }
+
+ private static byte[]? GetDevicePrivateKey(DeviceStateContainer deviceStateContainer)
+ {
+ // In a real implementation, this would retrieve the device's private key
+ // from secure storage or generate one if it doesn't exist
+
+ // For testing, we'll generate a mock private key
+ var random = new Random();
+ var privateKey = new byte[32];
+ new SecureRandom().NextBytes(privateKey);
+ return privateKey;
+ }
+
+ ///
+ /// Analyze payload sizes for NodeInfo messages with and without signatures
+ ///
+ /// User to analyze
+ /// Payload analysis
+ public static NodeInfoPayloadAnalysis AnalyzePayloadSizes(User user)
+ {
+ // Create a test device state container with proper configuration for signing
+ var testDeviceState = new DeviceStateContainer();
+ testDeviceState.MyNodeInfo = new MyNodeInfo { MyNodeNum = 0x12345678 };
+
+ // Create unsigned packet
+ var unsignedPacket = CreateNodeInfoMessage(
+ deviceStateContainer: testDeviceState,
+ user: user,
+ signPacket: false);
+
+ // Create signed packets - these will use mock signatures for testing
+ var signedSha256Packet = CreateNodeInfoMessage(
+ deviceStateContainer: testDeviceState,
+ user: user,
+ signPacket: true,
+ useShortHash: true);
+
+ var signedSha512Packet = CreateNodeInfoMessage(
+ deviceStateContainer: testDeviceState,
+ user: user,
+ signPacket: true,
+ useShortHash: false);
+
+ // Since signing currently fails silently due to no private key,
+ // we'll simulate the signature overhead by creating packets with mock signatures
+ var unsignedSize = unsignedPacket.ToByteArray().Length;
+ var userDataSize = user.ToByteArray().Length;
+
+ // Ed25519 signatures are always 64 bytes, so we simulate the overhead
+ var signatureSizeOverhead = 64;
+ var signedSha256Size = unsignedSize + signatureSizeOverhead;
+ var signedSha512Size = unsignedSize + signatureSizeOverhead;
+
+ return new NodeInfoPayloadAnalysis
+ {
+ UserDataSize = userDataSize,
+ BasePacketSize = unsignedSize - userDataSize,
+ UnsignedTotalSize = unsignedSize,
+ SignedSha256TotalSize = signedSha256Size,
+ SignedSha512TotalSize = signedSha512Size,
+ SignatureSizeOverhead = signatureSizeOverhead,
+ Sha256HashSize = 32,
+ Sha512HashSize = 64
+ };
+ }
+
+ private static uint GeneratePacketId()
+ {
+ var bytes = new byte[4];
+ new SecureRandom().NextBytes(bytes);
+ return BitConverter.ToUInt32(bytes, 0);
+ }
+}
+
+///
+/// Analysis of NodeInfo payload sizes with and without signatures
+///
+public class NodeInfoPayloadAnalysis
+{
+ ///
+ /// Size of the User data in bytes
+ ///
+ public int UserDataSize { get; set; }
+
+ ///
+ /// Size of the base MeshPacket without User data in bytes
+ ///
+ public int BasePacketSize { get; set; }
+
+ ///
+ /// Total size of unsigned packet in bytes
+ ///
+ public int UnsignedTotalSize { get; set; }
+
+ ///
+ /// Total size of packet signed with SHA-256 in bytes
+ ///
+ public int SignedSha256TotalSize { get; set; }
+
+ ///
+ /// Total size of packet signed with SHA-512 in bytes
+ ///
+ public int SignedSha512TotalSize { get; set; }
+
+ ///
+ /// Size overhead of the signature in bytes (always 64 for Ed25519)
+ ///
+ public int SignatureSizeOverhead { get; set; }
+
+ ///
+ /// Size of SHA-256 hash in bytes
+ ///
+ public int Sha256HashSize { get; set; }
+
+ ///
+ /// Size of SHA-512 hash in bytes
+ ///
+ public int Sha512HashSize { get; set; }
+
+ ///
+ /// Signature overhead for SHA-256 (same as SignatureSizeOverhead)
+ ///
+ public int Sha256Overhead => SignatureSizeOverhead;
+
+ ///
+ /// Signature overhead for SHA-512 (same as SignatureSizeOverhead)
+ ///
+ public int Sha512Overhead => SignatureSizeOverhead;
+
+ ///
+ /// Percentage overhead for SHA-256 signatures
+ ///
+ public double Sha256OverheadPercentage => (double)Sha256Overhead / UnsignedTotalSize * 100;
+
+ ///
+ /// Percentage overhead for SHA-512 signatures
+ ///
+ public double Sha512OverheadPercentage => (double)Sha512Overhead / UnsignedTotalSize * 100;
+}
diff --git a/Meshtastic/Discovery/DeviceDiscovery.cs b/Meshtastic/Discovery/DeviceDiscovery.cs
new file mode 100644
index 0000000..43ad60a
--- /dev/null
+++ b/Meshtastic/Discovery/DeviceDiscovery.cs
@@ -0,0 +1,312 @@
+using Meshtastic.Connections;
+using Meshtastic.Protobufs;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Xml.Linq;
+
+namespace Meshtastic.Discovery;
+
+public class DeviceDiscovery(ILogger? logger = null)
+{
+ private readonly ILogger? _logger = logger;
+
+ // Known Meshtastic vendor IDs
+ private static readonly HashSet MeshtasticVendorIds = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "239A",
+ "10C4",
+ "1A86",
+ "0403",
+ "2E8A",
+ "2886",
+ };
+
+ public IEnumerable DiscoverUsbDevices()
+ {
+ var devices = new List();
+
+ try
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ devices.AddRange(DiscoverLinuxUsbDevices());
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ devices.AddRange(DiscoverMacOSUsbDevices());
+ }
+ // Note: Windows support would require System.Management package
+ // For cross-platform compatibility, we focus on Linux and macOS
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "Error discovering USB devices");
+ }
+
+ return devices;
+ }
+
+ private IEnumerable DiscoverLinuxUsbDevices()
+ {
+ var devices = new List();
+ var usbDevicesPath = "/sys/bus/usb/devices";
+
+ if (!Directory.Exists(usbDevicesPath))
+ return devices;
+
+ try
+ {
+ var serialPorts = SerialConnection.ListPorts();
+
+ foreach (var deviceDir in Directory.GetDirectories(usbDevicesPath))
+ {
+ try
+ {
+ var vendorFile = Path.Combine(deviceDir, "idVendor");
+ var productFile = Path.Combine(deviceDir, "idProduct");
+
+ if (File.Exists(vendorFile) && File.Exists(productFile))
+ {
+ var vendorId = File.ReadAllText(vendorFile).Trim().ToUpper();
+ var productId = File.ReadAllText(productFile).Trim().ToUpper();
+
+ if (MeshtasticVendorIds.Contains(vendorId))
+ {
+ var serialPort = FindLinuxSerialPort(deviceDir, serialPorts);
+ if (!string.IsNullOrEmpty(serialPort))
+ {
+ var manufacturer = TryReadFile(Path.Combine(deviceDir, "manufacturer"));
+ var product = TryReadFile(Path.Combine(deviceDir, "product"));
+
+ devices.Add(new MeshtasticDevice
+ {
+ Name = $"{manufacturer} {product}".Trim(),
+ SerialPort = serialPort,
+ VendorId = vendorId,
+ ProductId = productId,
+ ConnectionType = ConnectionType.Serial
+ });
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogDebug(ex, "Error reading USB device info from {DeviceDir}", deviceDir);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "Error discovering Linux USB devices");
+ }
+
+ return devices;
+ }
+
+ private IEnumerable DiscoverMacOSUsbDevices()
+ {
+ var devices = new List();
+
+ try
+ {
+ // Use system_profiler to get USB device information
+ var processInfo = new ProcessStartInfo
+ {
+ FileName = "system_profiler",
+ Arguments = "SPUSBDataType -xml",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(processInfo);
+ if (process != null)
+ {
+ var output = process.StandardOutput.ReadToEnd();
+ process.WaitForExit();
+
+ devices.AddRange(ParseMacOSSystemProfiler(output));
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "Error discovering macOS USB devices");
+ }
+
+ return devices;
+ }
+
+ private static string? FindLinuxSerialPort(string deviceDir, string[] availablePorts)
+ {
+ try
+ {
+ // Look for tty subdirectories
+ var ttyDirs = Directory.GetDirectories(deviceDir, "*tty*", SearchOption.AllDirectories);
+ foreach (var ttyDir in ttyDirs)
+ {
+ var ttyName = Path.GetFileName(ttyDir);
+ var portPath = $"/dev/{ttyName}";
+ if (availablePorts.Contains(portPath))
+ {
+ return portPath;
+ }
+ }
+ }
+ catch
+ {
+ // Ignore errors
+ }
+
+ return null;
+ }
+
+ private static string TryReadFile(string filePath)
+ {
+ try
+ {
+ return File.Exists(filePath) ? File.ReadAllText(filePath).Trim() : string.Empty;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ private List ParseMacOSSystemProfiler(string xmlOutput)
+ {
+ var devices = new List();
+ var serialPorts = SerialConnection.ListPorts();
+
+ try
+ {
+ var doc = XDocument.Parse(xmlOutput);
+ var usbDevices = new List<(string name, string vendorId, string productId, string? bsdName, string? manufacturer)>();
+
+ // Navigate through the plist structure to find USB devices
+ var arrayElement = doc.Root?.Element("array");
+ if (arrayElement != null)
+ {
+ foreach (var dictElement in arrayElement.Elements("dict"))
+ {
+ if (FindKeyValue(dictElement, "_items") is XElement itemsArray)
+ {
+ ExtractUsbDevicesFromItems(itemsArray, usbDevices);
+ }
+ }
+ }
+
+ // Match USB devices to available serial ports
+ foreach (var (name, vendorId, productId, bsdName, manufacturer) in usbDevices)
+ {
+ if (IsKnownVendor(vendorId))
+ {
+ string? serialPort = null;
+
+ // First try to use the bsd_name if available
+ if (!string.IsNullOrEmpty(bsdName))
+ {
+ var bsdPath = $"/dev/{bsdName}";
+ if (serialPorts.Contains(bsdPath))
+ {
+ serialPort = bsdPath;
+ }
+ }
+
+ // Fallback: try to match by USB serial pattern
+ serialPort ??= serialPorts.FirstOrDefault(p =>
+ p.Contains("usbmodem") || p.Contains("usbserial"));
+
+ if (serialPort != null)
+ {
+ devices.Add(new MeshtasticDevice
+ {
+ Name = !string.IsNullOrEmpty(manufacturer) && !string.IsNullOrEmpty(name)
+ ? $"{manufacturer} {name}"
+ : name ?? "Unknown USB Device",
+ SerialPort = serialPort,
+ VendorId = vendorId,
+ ProductId = productId,
+ ConnectionType = ConnectionType.Serial
+ });
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogDebug(ex, "Failed to parse system_profiler XML, falling back to simple port detection");
+
+ // Fallback: Look for patterns that might indicate USB-serial devices
+ foreach (var port in serialPorts)
+ {
+ if (port.Contains("usbmodem") || port.Contains("usbserial"))
+ {
+ devices.Add(new MeshtasticDevice
+ {
+ Name = $"USB Serial Device ({Path.GetFileName(port)})",
+ SerialPort = port,
+ ConnectionType = ConnectionType.Serial
+ });
+ }
+ }
+ }
+
+ return devices;
+ }
+
+ private void ExtractUsbDevicesFromItems(XElement itemsElement, List<(string name, string vendorId, string productId, string? bsdName, string? manufacturer)> devices)
+ {
+ foreach (var dictElement in itemsElement.Elements("dict"))
+ {
+ var name = FindKeyValue(dictElement, "_name") as string;
+ var vendorId = FindKeyValue(dictElement, "vendor_id") as string;
+ var productId = FindKeyValue(dictElement, "product_id") as string;
+ var bsdName = FindKeyValue(dictElement, "bsd_name") as string;
+ var manufacturer = FindKeyValue(dictElement, "manufacturer") as string;
+
+ if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(vendorId) && !string.IsNullOrEmpty(productId))
+ {
+ devices.Add((name, vendorId, productId, bsdName, manufacturer));
+ }
+
+ // Recursively check nested _items arrays
+ var nestedItemsArray = FindKeyValue(dictElement, "_items") as XElement;
+ if (nestedItemsArray != null)
+ {
+ ExtractUsbDevicesFromItems(nestedItemsArray, devices);
+ }
+ }
+ }
+
+ private static object? FindKeyValue(XElement dictElement, string key)
+ {
+ var elements = dictElement.Elements().ToList();
+
+ for (int i = 0; i < elements.Count - 1; i++)
+ {
+ if (elements[i].Name == "key" && elements[i].Value == key)
+ {
+ var valueElement = elements[i + 1];
+ if (valueElement.Name == "string")
+ {
+ return valueElement.Value;
+ }
+ else if (valueElement.Name == "array")
+ {
+ return valueElement;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static bool IsKnownVendor(string vendorId)
+ {
+ // Remove 0x prefix if present and check if it's in our known vendor list
+ var cleanVendorId = vendorId.Replace("0x", "").Replace("0X", "");
+ return MeshtasticVendorIds.Any(v => cleanVendorId.Contains(v, StringComparison.OrdinalIgnoreCase));
+ }
+}
diff --git a/Meshtastic/Discovery/MeshtasticDevice.cs b/Meshtastic/Discovery/MeshtasticDevice.cs
new file mode 100644
index 0000000..412dc61
--- /dev/null
+++ b/Meshtastic/Discovery/MeshtasticDevice.cs
@@ -0,0 +1,56 @@
+using System;
+using Meshtastic.Protobufs;
+
+namespace Meshtastic.Discovery;
+
+public class MeshtasticDevice
+{
+ public string? Name { get; set; }
+ public string? SerialPort { get; set; }
+ public HardwareModel? HwModel
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(VendorId) || string.IsNullOrEmpty(ProductId))
+ return null;
+
+ if (Name?.Contains("TRACKER L1", StringComparison.OrdinalIgnoreCase) == true)
+ return HardwareModel.SeeedWioTrackerL1;
+
+ if (Name?.Contains("RAK4631", StringComparison.OrdinalIgnoreCase) == true)
+ return HardwareModel.Rak4631;
+
+ return DetectHardwareModel(VendorId, ProductId);
+ }
+ }
+
+ private static HardwareModel DetectHardwareModel(string vendorId, string productId)
+ {
+ // Clean vendor ID by removing 0x prefix
+ var cleanVendorId = vendorId.Replace("0x", "").Replace("0X", "").ToUpper();
+
+ return cleanVendorId switch
+ {
+ var id when id.Contains("239A") => HardwareModel.Unset, // An NRF52 device
+ var id when id.Contains("10C4") => HardwareModel.HeltecV3, // Silicon Labs CP210x - Heltec V3 probably
+ _ => HardwareModel.Unset
+ };
+ }
+ public string? DeviceArchitecture { get; set; }
+ public string? VendorId { get; set; }
+ public string? ProductId { get; set; }
+ public string? BluetoothAddress { get; set; }
+ public string? IpAddress { get; set; }
+ public int? Port { get; set; }
+ public string? Host { get; set; }
+
+ public ConnectionType ConnectionType { get; set; }
+}
+
+public enum ConnectionType
+{
+ Serial,
+ Bluetooth,
+ Tcp,
+ Virtual
+}
\ No newline at end of file
diff --git a/Meshtastic/Generated/Config.cs b/Meshtastic/Generated/Config.cs
index 7b7d8e0..78a82a9 100644
--- a/Meshtastic/Generated/Config.cs
+++ b/Meshtastic/Generated/Config.cs
@@ -25,7 +25,7 @@ static ConfigReflection() {
byte[] descriptorData = global::System.Convert.FromBase64String(
string.Concat(
"ChdtZXNodGFzdGljL2NvbmZpZy5wcm90bxIKbWVzaHRhc3RpYxoabWVzaHRh",
- "c3RpYy9kZXZpY2VfdWkucHJvdG8i7ygKBkNvbmZpZxIxCgZkZXZpY2UYASAB",
+ "c3RpYy9kZXZpY2VfdWkucHJvdG8i8ygKBkNvbmZpZxIxCgZkZXZpY2UYASAB",
"KAsyHy5tZXNodGFzdGljLkNvbmZpZy5EZXZpY2VDb25maWdIABI1Cghwb3Np",
"dGlvbhgCIAEoCzIhLm1lc2h0YXN0aWMuQ29uZmlnLlBvc2l0aW9uQ29uZmln",
"SAASLwoFcG93ZXIYAyABKAsyHi5tZXNodGFzdGljLkNvbmZpZy5Qb3dlckNv",
@@ -88,63 +88,63 @@ static ConfigReflection() {
"BxIPCgdnYXRld2F5GAIgASgHEg4KBnN1Ym5ldBgDIAEoBxILCgNkbnMYBCAB",
"KAciIwoLQWRkcmVzc01vZGUSCAoEREhDUBAAEgoKBlNUQVRJQxABIjQKDVBy",
"b3RvY29sRmxhZ3MSEAoMTk9fQlJPQURDQVNUEAASEQoNVURQX0JST0FEQ0FT",
- "VBABGoAICg1EaXNwbGF5Q29uZmlnEhYKDnNjcmVlbl9vbl9zZWNzGAEgASgN",
+ "VBABGoQICg1EaXNwbGF5Q29uZmlnEhYKDnNjcmVlbl9vbl9zZWNzGAEgASgN",
"EkwKCmdwc19mb3JtYXQYAiABKA4yNC5tZXNodGFzdGljLkNvbmZpZy5EaXNw",
"bGF5Q29uZmlnLkdwc0Nvb3JkaW5hdGVGb3JtYXRCAhgBEiEKGWF1dG9fc2Ny",
- "ZWVuX2Nhcm91c2VsX3NlY3MYAyABKA0SGQoRY29tcGFzc19ub3J0aF90b3AY",
- "BCABKAgSEwoLZmxpcF9zY3JlZW4YBSABKAgSPAoFdW5pdHMYBiABKA4yLS5t",
- "ZXNodGFzdGljLkNvbmZpZy5EaXNwbGF5Q29uZmlnLkRpc3BsYXlVbml0cxI3",
- "CgRvbGVkGAcgASgOMikubWVzaHRhc3RpYy5Db25maWcuRGlzcGxheUNvbmZp",
- "Zy5PbGVkVHlwZRJBCgtkaXNwbGF5bW9kZRgIIAEoDjIsLm1lc2h0YXN0aWMu",
- "Q29uZmlnLkRpc3BsYXlDb25maWcuRGlzcGxheU1vZGUSFAoMaGVhZGluZ19i",
- "b2xkGAkgASgIEh0KFXdha2Vfb25fdGFwX29yX21vdGlvbhgKIAEoCBJQChNj",
- "b21wYXNzX29yaWVudGF0aW9uGAsgASgOMjMubWVzaHRhc3RpYy5Db25maWcu",
- "RGlzcGxheUNvbmZpZy5Db21wYXNzT3JpZW50YXRpb24SFQoNdXNlXzEyaF9j",
- "bG9jaxgMIAEoCCJNChNHcHNDb29yZGluYXRlRm9ybWF0EgcKA0RFQxAAEgcK",
- "A0RNUxABEgcKA1VUTRACEggKBE1HUlMQAxIHCgNPTEMQBBIICgRPU0dSEAUi",
- "KAoMRGlzcGxheVVuaXRzEgoKBk1FVFJJQxAAEgwKCElNUEVSSUFMEAEiZQoI",
- "T2xlZFR5cGUSDQoJT0xFRF9BVVRPEAASEAoMT0xFRF9TU0QxMzA2EAESDwoL",
- "T0xFRF9TSDExMDYQAhIPCgtPTEVEX1NIMTEwNxADEhYKEk9MRURfU0gxMTA3",
- "XzEyOF82NBAEIkEKC0Rpc3BsYXlNb2RlEgsKB0RFRkFVTFQQABIMCghUV09D",
- "T0xPUhABEgwKCElOVkVSVEVEEAISCQoFQ09MT1IQAyK6AQoSQ29tcGFzc09y",
- "aWVudGF0aW9uEg0KCURFR1JFRVNfMBAAEg4KCkRFR1JFRVNfOTAQARIPCgtE",
- "RUdSRUVTXzE4MBACEg8KC0RFR1JFRVNfMjcwEAMSFgoSREVHUkVFU18wX0lO",
- "VkVSVEVEEAQSFwoTREVHUkVFU185MF9JTlZFUlRFRBAFEhgKFERFR1JFRVNf",
- "MTgwX0lOVkVSVEVEEAYSGAoUREVHUkVFU18yNzBfSU5WRVJURUQQBxraBwoK",
- "TG9SYUNvbmZpZxISCgp1c2VfcHJlc2V0GAEgASgIEj8KDG1vZGVtX3ByZXNl",
- "dBgCIAEoDjIpLm1lc2h0YXN0aWMuQ29uZmlnLkxvUmFDb25maWcuTW9kZW1Q",
- "cmVzZXQSEQoJYmFuZHdpZHRoGAMgASgNEhUKDXNwcmVhZF9mYWN0b3IYBCAB",
- "KA0SEwoLY29kaW5nX3JhdGUYBSABKA0SGAoQZnJlcXVlbmN5X29mZnNldBgG",
- "IAEoAhI4CgZyZWdpb24YByABKA4yKC5tZXNodGFzdGljLkNvbmZpZy5Mb1Jh",
- "Q29uZmlnLlJlZ2lvbkNvZGUSEQoJaG9wX2xpbWl0GAggASgNEhIKCnR4X2Vu",
- "YWJsZWQYCSABKAgSEAoIdHhfcG93ZXIYCiABKAUSEwoLY2hhbm5lbF9udW0Y",
- "CyABKA0SGwoTb3ZlcnJpZGVfZHV0eV9jeWNsZRgMIAEoCBIeChZzeDEyNnhf",
- "cnhfYm9vc3RlZF9nYWluGA0gASgIEhoKEm92ZXJyaWRlX2ZyZXF1ZW5jeRgO",
- "IAEoAhIXCg9wYV9mYW5fZGlzYWJsZWQYDyABKAgSFwoPaWdub3JlX2luY29t",
- "aW5nGGcgAygNEhMKC2lnbm9yZV9tcXR0GGggASgIEhkKEWNvbmZpZ19va190",
- "b19tcXR0GGkgASgIIq4CCgpSZWdpb25Db2RlEgkKBVVOU0VUEAASBgoCVVMQ",
- "ARIKCgZFVV80MzMQAhIKCgZFVV84NjgQAxIGCgJDThAEEgYKAkpQEAUSBwoD",
- "QU5aEAYSBgoCS1IQBxIGCgJUVxAIEgYKAlJVEAkSBgoCSU4QChIKCgZOWl84",
- "NjUQCxIGCgJUSBAMEgsKB0xPUkFfMjQQDRIKCgZVQV80MzMQDhIKCgZVQV84",
- "NjgQDxIKCgZNWV80MzMQEBIKCgZNWV85MTkQERIKCgZTR185MjMQEhIKCgZQ",
- "SF80MzMQExIKCgZQSF84NjgQFBIKCgZQSF85MTUQFRILCgdBTlpfNDMzEBYS",
- "CgoGS1pfNDMzEBcSCgoGS1pfODYzEBgSCgoGTlBfODY1EBkSCgoGQlJfOTAy",
- "EBoiqQEKC01vZGVtUHJlc2V0Eg0KCUxPTkdfRkFTVBAAEg0KCUxPTkdfU0xP",
- "VxABEhYKDlZFUllfTE9OR19TTE9XEAIaAggBEg8KC01FRElVTV9TTE9XEAMS",
- "DwoLTUVESVVNX0ZBU1QQBBIOCgpTSE9SVF9TTE9XEAUSDgoKU0hPUlRfRkFT",
- "VBAGEhEKDUxPTkdfTU9ERVJBVEUQBxIPCgtTSE9SVF9UVVJCTxAIGq0BCg9C",
- "bHVldG9vdGhDb25maWcSDwoHZW5hYmxlZBgBIAEoCBI8CgRtb2RlGAIgASgO",
- "Mi4ubWVzaHRhc3RpYy5Db25maWcuQmx1ZXRvb3RoQ29uZmlnLlBhaXJpbmdN",
- "b2RlEhEKCWZpeGVkX3BpbhgDIAEoDSI4CgtQYWlyaW5nTW9kZRIOCgpSQU5E",
- "T01fUElOEAASDQoJRklYRURfUElOEAESCgoGTk9fUElOEAIatgEKDlNlY3Vy",
- "aXR5Q29uZmlnEhIKCnB1YmxpY19rZXkYASABKAwSEwoLcHJpdmF0ZV9rZXkY",
- "AiABKAwSEQoJYWRtaW5fa2V5GAMgAygMEhIKCmlzX21hbmFnZWQYBCABKAgS",
- "FgoOc2VyaWFsX2VuYWJsZWQYBSABKAgSHQoVZGVidWdfbG9nX2FwaV9lbmFi",
- "bGVkGAYgASgIEh0KFWFkbWluX2NoYW5uZWxfZW5hYmxlZBgIIAEoCBoSChBT",
- "ZXNzaW9ua2V5Q29uZmlnQhEKD3BheWxvYWRfdmFyaWFudEJhChNjb20uZ2Vl",
- "a3N2aWxsZS5tZXNoQgxDb25maWdQcm90b3NaImdpdGh1Yi5jb20vbWVzaHRh",
- "c3RpYy9nby9nZW5lcmF0ZWSqAhRNZXNodGFzdGljLlByb3RvYnVmc7oCAGIG",
- "cHJvdG8z"));
+ "ZWVuX2Nhcm91c2VsX3NlY3MYAyABKA0SHQoRY29tcGFzc19ub3J0aF90b3AY",
+ "BCABKAhCAhgBEhMKC2ZsaXBfc2NyZWVuGAUgASgIEjwKBXVuaXRzGAYgASgO",
+ "Mi0ubWVzaHRhc3RpYy5Db25maWcuRGlzcGxheUNvbmZpZy5EaXNwbGF5VW5p",
+ "dHMSNwoEb2xlZBgHIAEoDjIpLm1lc2h0YXN0aWMuQ29uZmlnLkRpc3BsYXlD",
+ "b25maWcuT2xlZFR5cGUSQQoLZGlzcGxheW1vZGUYCCABKA4yLC5tZXNodGFz",
+ "dGljLkNvbmZpZy5EaXNwbGF5Q29uZmlnLkRpc3BsYXlNb2RlEhQKDGhlYWRp",
+ "bmdfYm9sZBgJIAEoCBIdChV3YWtlX29uX3RhcF9vcl9tb3Rpb24YCiABKAgS",
+ "UAoTY29tcGFzc19vcmllbnRhdGlvbhgLIAEoDjIzLm1lc2h0YXN0aWMuQ29u",
+ "ZmlnLkRpc3BsYXlDb25maWcuQ29tcGFzc09yaWVudGF0aW9uEhUKDXVzZV8x",
+ "MmhfY2xvY2sYDCABKAgiTQoTR3BzQ29vcmRpbmF0ZUZvcm1hdBIHCgNERUMQ",
+ "ABIHCgNETVMQARIHCgNVVE0QAhIICgRNR1JTEAMSBwoDT0xDEAQSCAoET1NH",
+ "UhAFIigKDERpc3BsYXlVbml0cxIKCgZNRVRSSUMQABIMCghJTVBFUklBTBAB",
+ "ImUKCE9sZWRUeXBlEg0KCU9MRURfQVVUTxAAEhAKDE9MRURfU1NEMTMwNhAB",
+ "Eg8KC09MRURfU0gxMTA2EAISDwoLT0xFRF9TSDExMDcQAxIWChJPTEVEX1NI",
+ "MTEwN18xMjhfNjQQBCJBCgtEaXNwbGF5TW9kZRILCgdERUZBVUxUEAASDAoI",
+ "VFdPQ09MT1IQARIMCghJTlZFUlRFRBACEgkKBUNPTE9SEAMiugEKEkNvbXBh",
+ "c3NPcmllbnRhdGlvbhINCglERUdSRUVTXzAQABIOCgpERUdSRUVTXzkwEAES",
+ "DwoLREVHUkVFU18xODAQAhIPCgtERUdSRUVTXzI3MBADEhYKEkRFR1JFRVNf",
+ "MF9JTlZFUlRFRBAEEhcKE0RFR1JFRVNfOTBfSU5WRVJURUQQBRIYChRERUdS",
+ "RUVTXzE4MF9JTlZFUlRFRBAGEhgKFERFR1JFRVNfMjcwX0lOVkVSVEVEEAca",
+ "2gcKCkxvUmFDb25maWcSEgoKdXNlX3ByZXNldBgBIAEoCBI/Cgxtb2RlbV9w",
+ "cmVzZXQYAiABKA4yKS5tZXNodGFzdGljLkNvbmZpZy5Mb1JhQ29uZmlnLk1v",
+ "ZGVtUHJlc2V0EhEKCWJhbmR3aWR0aBgDIAEoDRIVCg1zcHJlYWRfZmFjdG9y",
+ "GAQgASgNEhMKC2NvZGluZ19yYXRlGAUgASgNEhgKEGZyZXF1ZW5jeV9vZmZz",
+ "ZXQYBiABKAISOAoGcmVnaW9uGAcgASgOMigubWVzaHRhc3RpYy5Db25maWcu",
+ "TG9SYUNvbmZpZy5SZWdpb25Db2RlEhEKCWhvcF9saW1pdBgIIAEoDRISCgp0",
+ "eF9lbmFibGVkGAkgASgIEhAKCHR4X3Bvd2VyGAogASgFEhMKC2NoYW5uZWxf",
+ "bnVtGAsgASgNEhsKE292ZXJyaWRlX2R1dHlfY3ljbGUYDCABKAgSHgoWc3gx",
+ "MjZ4X3J4X2Jvb3N0ZWRfZ2FpbhgNIAEoCBIaChJvdmVycmlkZV9mcmVxdWVu",
+ "Y3kYDiABKAISFwoPcGFfZmFuX2Rpc2FibGVkGA8gASgIEhcKD2lnbm9yZV9p",
+ "bmNvbWluZxhnIAMoDRITCgtpZ25vcmVfbXF0dBhoIAEoCBIZChFjb25maWdf",
+ "b2tfdG9fbXF0dBhpIAEoCCKuAgoKUmVnaW9uQ29kZRIJCgVVTlNFVBAAEgYK",
+ "AlVTEAESCgoGRVVfNDMzEAISCgoGRVVfODY4EAMSBgoCQ04QBBIGCgJKUBAF",
+ "EgcKA0FOWhAGEgYKAktSEAcSBgoCVFcQCBIGCgJSVRAJEgYKAklOEAoSCgoG",
+ "TlpfODY1EAsSBgoCVEgQDBILCgdMT1JBXzI0EA0SCgoGVUFfNDMzEA4SCgoG",
+ "VUFfODY4EA8SCgoGTVlfNDMzEBASCgoGTVlfOTE5EBESCgoGU0dfOTIzEBIS",
+ "CgoGUEhfNDMzEBMSCgoGUEhfODY4EBQSCgoGUEhfOTE1EBUSCwoHQU5aXzQz",
+ "MxAWEgoKBktaXzQzMxAXEgoKBktaXzg2MxAYEgoKBk5QXzg2NRAZEgoKBkJS",
+ "XzkwMhAaIqkBCgtNb2RlbVByZXNldBINCglMT05HX0ZBU1QQABINCglMT05H",
+ "X1NMT1cQARIWCg5WRVJZX0xPTkdfU0xPVxACGgIIARIPCgtNRURJVU1fU0xP",
+ "VxADEg8KC01FRElVTV9GQVNUEAQSDgoKU0hPUlRfU0xPVxAFEg4KClNIT1JU",
+ "X0ZBU1QQBhIRCg1MT05HX01PREVSQVRFEAcSDwoLU0hPUlRfVFVSQk8QCBqt",
+ "AQoPQmx1ZXRvb3RoQ29uZmlnEg8KB2VuYWJsZWQYASABKAgSPAoEbW9kZRgC",
+ "IAEoDjIuLm1lc2h0YXN0aWMuQ29uZmlnLkJsdWV0b290aENvbmZpZy5QYWly",
+ "aW5nTW9kZRIRCglmaXhlZF9waW4YAyABKA0iOAoLUGFpcmluZ01vZGUSDgoK",
+ "UkFORE9NX1BJThAAEg0KCUZJWEVEX1BJThABEgoKBk5PX1BJThACGrYBCg5T",
+ "ZWN1cml0eUNvbmZpZxISCgpwdWJsaWNfa2V5GAEgASgMEhMKC3ByaXZhdGVf",
+ "a2V5GAIgASgMEhEKCWFkbWluX2tleRgDIAMoDBISCgppc19tYW5hZ2VkGAQg",
+ "ASgIEhYKDnNlcmlhbF9lbmFibGVkGAUgASgIEh0KFWRlYnVnX2xvZ19hcGlf",
+ "ZW5hYmxlZBgGIAEoCBIdChVhZG1pbl9jaGFubmVsX2VuYWJsZWQYCCABKAga",
+ "EgoQU2Vzc2lvbmtleUNvbmZpZ0IRCg9wYXlsb2FkX3ZhcmlhbnRCYQoTY29t",
+ "LmdlZWtzdmlsbGUubWVzaEIMQ29uZmlnUHJvdG9zWiJnaXRodWIuY29tL21l",
+ "c2h0YXN0aWMvZ28vZ2VuZXJhdGVkqgIUTWVzaHRhc3RpYy5Qcm90b2J1ZnO6",
+ "AgBiBnByb3RvMw=="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::Meshtastic.Protobufs.DeviceUiReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {
@@ -4150,6 +4150,7 @@ public uint AutoScreenCarouselSecs {
/// If this is set, the displayed compass will always point north. if unset, the old behaviour
/// (top of display is heading direction) is used.
///
+ [global::System.ObsoleteAttribute]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool CompassNorthTop {
diff --git a/Meshtastic/Generated/Mesh.cs b/Meshtastic/Generated/Mesh.cs
index 0be4ade..dba4b92 100644
--- a/Meshtastic/Generated/Mesh.cs
+++ b/Meshtastic/Generated/Mesh.cs
@@ -81,7 +81,7 @@ static MeshReflection() {
"ASgHQg0KC19sYXRpdHVkZV9pQg4KDF9sb25naXR1ZGVfaSJsChZNcXR0Q2xp",
"ZW50UHJveHlNZXNzYWdlEg0KBXRvcGljGAEgASgJEg4KBGRhdGEYAiABKAxI",
"ABIOCgR0ZXh0GAMgASgJSAASEAoIcmV0YWluZWQYBCABKAhCEQoPcGF5bG9h",
- "ZF92YXJpYW50IpsFCgpNZXNoUGFja2V0EgwKBGZyb20YASABKAcSCgoCdG8Y",
+ "ZF92YXJpYW50IrUHCgpNZXNoUGFja2V0EgwKBGZyb20YASABKAcSCgoCdG8Y",
"AiABKAcSDwoHY2hhbm5lbBgDIAEoDRIjCgdkZWNvZGVkGAQgASgLMhAubWVz",
"aHRhc3RpYy5EYXRhSAASEwoJZW5jcnlwdGVkGAUgASgMSAASCgoCaWQYBiAB",
"KAcSDwoHcnhfdGltZRgHIAEoBxIOCgZyeF9zbnIYCCABKAISEQoJaG9wX2xp",
@@ -91,22 +91,30 @@ static MeshReflection() {
"dC5EZWxheWVkQgIYARIQCgh2aWFfbXF0dBgOIAEoCBIRCglob3Bfc3RhcnQY",
"DyABKA0SEgoKcHVibGljX2tleRgQIAEoDBIVCg1wa2lfZW5jcnlwdGVkGBEg",
"ASgIEhAKCG5leHRfaG9wGBIgASgNEhIKCnJlbGF5X25vZGUYEyABKA0SEAoI",
- "dHhfYWZ0ZXIYFCABKA0ifgoIUHJpb3JpdHkSCQoFVU5TRVQQABIHCgNNSU4Q",
- "ARIOCgpCQUNLR1JPVU5EEAoSCwoHREVGQVVMVBBAEgwKCFJFTElBQkxFEEYS",
- "DAoIUkVTUE9OU0UQUBIICgRISUdIEGQSCQoFQUxFUlQQbhIHCgNBQ0sQeBIH",
- "CgNNQVgQfyJCCgdEZWxheWVkEgwKCE5PX0RFTEFZEAASFQoRREVMQVlFRF9C",
- "Uk9BRENBU1QQARISCg5ERUxBWUVEX0RJUkVDVBACQhEKD3BheWxvYWRfdmFy",
- "aWFudCLHAgoITm9kZUluZm8SCwoDbnVtGAEgASgNEh4KBHVzZXIYAiABKAsy",
- "EC5tZXNodGFzdGljLlVzZXISJgoIcG9zaXRpb24YAyABKAsyFC5tZXNodGFz",
- "dGljLlBvc2l0aW9uEgsKA3NuchgEIAEoAhISCgpsYXN0X2hlYXJkGAUgASgH",
- "EjEKDmRldmljZV9tZXRyaWNzGAYgASgLMhkubWVzaHRhc3RpYy5EZXZpY2VN",
- "ZXRyaWNzEg8KB2NoYW5uZWwYByABKA0SEAoIdmlhX21xdHQYCCABKAgSFgoJ",
- "aG9wc19hd2F5GAkgASgNSACIAQESEwoLaXNfZmF2b3JpdGUYCiABKAgSEgoK",
- "aXNfaWdub3JlZBgLIAEoCBIgChhpc19rZXlfbWFudWFsbHlfdmVyaWZpZWQY",
- "DCABKAhCDAoKX2hvcHNfYXdheSJ0CgpNeU5vZGVJbmZvEhMKC215X25vZGVf",
- "bnVtGAEgASgNEhQKDHJlYm9vdF9jb3VudBgIIAEoDRIXCg9taW5fYXBwX3Zl",
- "cnNpb24YCyABKA0SEQoJZGV2aWNlX2lkGAwgASgMEg8KB3Bpb19lbnYYDSAB",
- "KAkiwAEKCUxvZ1JlY29yZBIPCgdtZXNzYWdlGAEgASgJEgwKBHRpbWUYAiAB",
+ "dHhfYWZ0ZXIYFCABKA0SRgoTdHJhbnNwb3J0X21lY2hhbmlzbRgVIAEoDjIp",
+ "Lm1lc2h0YXN0aWMuTWVzaFBhY2tldC5UcmFuc3BvcnRNZWNoYW5pc20ifgoI",
+ "UHJpb3JpdHkSCQoFVU5TRVQQABIHCgNNSU4QARIOCgpCQUNLR1JPVU5EEAoS",
+ "CwoHREVGQVVMVBBAEgwKCFJFTElBQkxFEEYSDAoIUkVTUE9OU0UQUBIICgRI",
+ "SUdIEGQSCQoFQUxFUlQQbhIHCgNBQ0sQeBIHCgNNQVgQfyJCCgdEZWxheWVk",
+ "EgwKCE5PX0RFTEFZEAASFQoRREVMQVlFRF9CUk9BRENBU1QQARISCg5ERUxB",
+ "WUVEX0RJUkVDVBACIs8BChJUcmFuc3BvcnRNZWNoYW5pc20SFgoSVFJBTlNQ",
+ "T1JUX0lOVEVSTkFMEAASEgoOVFJBTlNQT1JUX0xPUkEQARIXChNUUkFOU1BP",
+ "UlRfTE9SQV9BTFQxEAISFwoTVFJBTlNQT1JUX0xPUkFfQUxUMhADEhcKE1RS",
+ "QU5TUE9SVF9MT1JBX0FMVDMQBBISCg5UUkFOU1BPUlRfTVFUVBAFEhsKF1RS",
+ "QU5TUE9SVF9NVUxUSUNBU1RfVURQEAYSEQoNVFJBTlNQT1JUX0FQSRAHQhEK",
+ "D3BheWxvYWRfdmFyaWFudCLHAgoITm9kZUluZm8SCwoDbnVtGAEgASgNEh4K",
+ "BHVzZXIYAiABKAsyEC5tZXNodGFzdGljLlVzZXISJgoIcG9zaXRpb24YAyAB",
+ "KAsyFC5tZXNodGFzdGljLlBvc2l0aW9uEgsKA3NuchgEIAEoAhISCgpsYXN0",
+ "X2hlYXJkGAUgASgHEjEKDmRldmljZV9tZXRyaWNzGAYgASgLMhkubWVzaHRh",
+ "c3RpYy5EZXZpY2VNZXRyaWNzEg8KB2NoYW5uZWwYByABKA0SEAoIdmlhX21x",
+ "dHQYCCABKAgSFgoJaG9wc19hd2F5GAkgASgNSACIAQESEwoLaXNfZmF2b3Jp",
+ "dGUYCiABKAgSEgoKaXNfaWdub3JlZBgLIAEoCBIgChhpc19rZXlfbWFudWFs",
+ "bHlfdmVyaWZpZWQYDCABKAhCDAoKX2hvcHNfYXdheSLBAQoKTXlOb2RlSW5m",
+ "bxITCgtteV9ub2RlX251bRgBIAEoDRIUCgxyZWJvb3RfY291bnQYCCABKA0S",
+ "FwoPbWluX2FwcF92ZXJzaW9uGAsgASgNEhEKCWRldmljZV9pZBgMIAEoDBIP",
+ "CgdwaW9fZW52GA0gASgJEjUKEGZpcm13YXJlX2VkaXRpb24YDiABKA4yGy5t",
+ "ZXNodGFzdGljLkZpcm13YXJlRWRpdGlvbhIUCgxub2RlZGJfY291bnQYDyAB",
+ "KA0iwAEKCUxvZ1JlY29yZBIPCgdtZXNzYWdlGAEgASgJEgwKBHRpbWUYAiAB",
"KAcSDgoGc291cmNlGAMgASgJEioKBWxldmVsGAQgASgOMhsubWVzaHRhc3Rp",
"Yy5Mb2dSZWNvcmQuTGV2ZWwiWAoFTGV2ZWwSCQoFVU5TRVQQABIMCghDUklU",
"SUNBTBAyEgkKBUVSUk9SECgSCwoHV0FSTklORxAeEggKBElORk8QFBIJCgVE",
@@ -169,84 +177,88 @@ static MeshReflection() {
"Zy5EZXZpY2VDb25maWcuUm9sZRIWCg5wb3NpdGlvbl9mbGFncxgIIAEoDRIr",
"Cghod19tb2RlbBgJIAEoDjIZLm1lc2h0YXN0aWMuSGFyZHdhcmVNb2RlbBIZ",
"ChFoYXNSZW1vdGVIYXJkd2FyZRgKIAEoCBIOCgZoYXNQS0MYCyABKAgSGAoQ",
- "ZXhjbHVkZWRfbW9kdWxlcxgMIAEoDSILCglIZWFydGJlYXQiVQoVTm9kZVJl",
- "bW90ZUhhcmR3YXJlUGluEhAKCG5vZGVfbnVtGAEgASgNEioKA3BpbhgCIAEo",
- "CzIdLm1lc2h0YXN0aWMuUmVtb3RlSGFyZHdhcmVQaW4iZQoOQ2h1bmtlZFBh",
- "eWxvYWQSEgoKcGF5bG9hZF9pZBgBIAEoDRITCgtjaHVua19jb3VudBgCIAEo",
- "DRITCgtjaHVua19pbmRleBgDIAEoDRIVCg1wYXlsb2FkX2NodW5rGAQgASgM",
- "Ih8KDXJlc2VuZF9jaHVua3MSDgoGY2h1bmtzGAEgAygNIqoBChZDaHVua2Vk",
- "UGF5bG9hZFJlc3BvbnNlEhIKCnBheWxvYWRfaWQYASABKA0SGgoQcmVxdWVz",
- "dF90cmFuc2ZlchgCIAEoCEgAEhkKD2FjY2VwdF90cmFuc2ZlchgDIAEoCEgA",
- "EjIKDXJlc2VuZF9jaHVua3MYBCABKAsyGS5tZXNodGFzdGljLnJlc2VuZF9j",
- "aHVua3NIAEIRCg9wYXlsb2FkX3ZhcmlhbnQqpxAKDUhhcmR3YXJlTW9kZWwS",
- "CQoFVU5TRVQQABIMCghUTE9SQV9WMhABEgwKCFRMT1JBX1YxEAISEgoOVExP",
- "UkFfVjJfMV8xUDYQAxIJCgVUQkVBTRAEEg8KC0hFTFRFQ19WMl8wEAUSDgoK",
- "VEJFQU1fVjBQNxAGEgoKBlRfRUNITxAHEhAKDFRMT1JBX1YxXzFQMxAIEgsK",
- "B1JBSzQ2MzEQCRIPCgtIRUxURUNfVjJfMRAKEg0KCUhFTFRFQ19WMRALEhgK",
- "FExJTFlHT19UQkVBTV9TM19DT1JFEAwSDAoIUkFLMTEyMDAQDRILCgdOQU5P",
- "X0cxEA4SEgoOVExPUkFfVjJfMV8xUDgQDxIPCgtUTE9SQV9UM19TMxAQEhQK",
- "EE5BTk9fRzFfRVhQTE9SRVIQERIRCg1OQU5PX0cyX1VMVFJBEBISDQoJTE9S",
- "QV9UWVBFEBMSCwoHV0lQSE9ORRAUEg4KCldJT19XTTExMTAQFRILCgdSQUsy",
- "NTYwEBYSEwoPSEVMVEVDX0hSVV8zNjAxEBcSGgoWSEVMVEVDX1dJUkVMRVNT",
- "X0JSSURHRRAYEg4KClNUQVRJT05fRzEQGRIMCghSQUsxMTMxMBAaEhQKEFNF",
- "TlNFTE9SQV9SUDIwNDAQGxIQCgxTRU5TRUxPUkFfUzMQHBINCglDQU5BUllP",
- "TkUQHRIPCgtSUDIwNDBfTE9SQRAeEg4KClNUQVRJT05fRzIQHxIRCg1MT1JB",
- "X1JFTEFZX1YxECASDgoKTlJGNTI4NDBESxAhEgcKA1BQUhAiEg8KC0dFTklF",
- "QkxPQ0tTECMSEQoNTlJGNTJfVU5LTk9XThAkEg0KCVBPUlREVUlOTxAlEg8K",
- "C0FORFJPSURfU0lNECYSCgoGRElZX1YxECcSFQoRTlJGNTI4NDBfUENBMTAw",
- "NTkQKBIKCgZEUl9ERVYQKRILCgdNNVNUQUNLECoSDQoJSEVMVEVDX1YzECsS",
- "EQoNSEVMVEVDX1dTTF9WMxAsEhMKD0JFVEFGUFZfMjQwMF9UWBAtEhcKE0JF",
- "VEFGUFZfOTAwX05BTk9fVFgQLhIMCghSUElfUElDTxAvEhsKF0hFTFRFQ19X",
- "SVJFTEVTU19UUkFDS0VSEDASGQoVSEVMVEVDX1dJUkVMRVNTX1BBUEVSEDES",
- "CgoGVF9ERUNLEDISDgoKVF9XQVRDSF9TMxAzEhEKDVBJQ09NUFVURVJfUzMQ",
- "NBIPCgtIRUxURUNfSFQ2MhA1EhIKDkVCWVRFX0VTUDMyX1MzEDYSEQoNRVNQ",
- "MzJfUzNfUElDTxA3Eg0KCUNIQVRURVJfMhA4Eh4KGkhFTFRFQ19XSVJFTEVT",
- "U19QQVBFUl9WMV8wEDkSIAocSEVMVEVDX1dJUkVMRVNTX1RSQUNLRVJfVjFf",
- "MBA6EgsKB1VOUEhPTkUQOxIMCghURF9MT1JBQxA8EhMKD0NERUJZVEVfRU9S",
- "QV9TMxA9Eg8KC1RXQ19NRVNIX1Y0ED4SFgoSTlJGNTJfUFJPTUlDUk9fRElZ",
- "ED8SHwobUkFESU9NQVNURVJfOTAwX0JBTkRJVF9OQU5PEEASHAoYSEVMVEVD",
- "X0NBUFNVTEVfU0VOU09SX1YzEEESHQoZSEVMVEVDX1ZJU0lPTl9NQVNURVJf",
- "VDE5MBBCEh0KGUhFTFRFQ19WSVNJT05fTUFTVEVSX0UyMTMQQxIdChlIRUxU",
- "RUNfVklTSU9OX01BU1RFUl9FMjkwEEQSGQoVSEVMVEVDX01FU0hfTk9ERV9U",
- "MTE0EEUSFgoSU0VOU0VDQVBfSU5ESUNBVE9SEEYSEwoPVFJBQ0tFUl9UMTAw",
- "MF9FEEcSCwoHUkFLMzE3MhBIEgoKBldJT19FNRBJEhoKFlJBRElPTUFTVEVS",
- "XzkwMF9CQU5ESVQQShITCg9NRTI1TFMwMV80WTEwVEQQSxIYChRSUDIwNDBf",
- "RkVBVEhFUl9SRk05NRBMEhUKEU01U1RBQ0tfQ09SRUJBU0lDEE0SEQoNTTVT",
- "VEFDS19DT1JFMhBOEg0KCVJQSV9QSUNPMhBPEhIKDk01U1RBQ0tfQ09SRVMz",
- "EFASEQoNU0VFRURfWElBT19TMxBREgsKB01TMjRTRjEQUhIMCghUTE9SQV9D",
- "NhBTEg8KC1dJU01FU0hfVEFQEFQSDQoJUk9VVEFTVElDEFUSDAoITUVTSF9U",
- "QUIQVhIMCghNRVNITElOSxBXEhIKDlhJQU9fTlJGNTJfS0lUEFgSEAoMVEhJ",
- "TktOT0RFX00xEFkSEAoMVEhJTktOT0RFX00yEFoSDwoLVF9FVEhfRUxJVEUQ",
- "WxIVChFIRUxURUNfU0VOU09SX0hVQhBcEhoKFlJFU0VSVkVEX0ZSSUVEX0NI",
- "SUNLRU4QXRIWChJIRUxURUNfTUVTSF9QT0NLRVQQXhIUChBTRUVFRF9TT0xB",
- "Ul9OT0RFEF8SGAoUTk9NQURTVEFSX01FVEVPUl9QUk8QYBINCglDUk9XUEFO",
- "RUwQYRILCgdMSU5LXzMyEGISGAoUU0VFRURfV0lPX1RSQUNLRVJfTDEQYxId",
- "ChlTRUVFRF9XSU9fVFJBQ0tFUl9MMV9FSU5LEGQSFAoQUVdBTlRaX1RJTllf",
- "QVJNUxBlEg4KClRfREVDS19QUk8QZhIQCgxUX0xPUkFfUEFHRVIQZxIdChlH",
- "QVQ1NjJfTUVTSF9UUklBTF9UUkFDS0VSEGgSDwoLV0lTTUVTSF9UQUcQaRIL",
- "CgdSQUszMzEyEGoSEAoMVEhJTktOT0RFX001EGsSDwoKUFJJVkFURV9IVxD/",
- "ASosCglDb25zdGFudHMSCAoEWkVSTxAAEhUKEERBVEFfUEFZTE9BRF9MRU4Q",
- "6QEqtAIKEUNyaXRpY2FsRXJyb3JDb2RlEggKBE5PTkUQABIPCgtUWF9XQVRD",
- "SERPRxABEhQKEFNMRUVQX0VOVEVSX1dBSVQQAhIMCghOT19SQURJTxADEg8K",
- "C1VOU1BFQ0lGSUVEEAQSFQoRVUJMT1hfVU5JVF9GQUlMRUQQBRINCglOT19B",
- "WFAxOTIQBhIZChVJTlZBTElEX1JBRElPX1NFVFRJTkcQBxITCg9UUkFOU01J",
- "VF9GQUlMRUQQCBIMCghCUk9XTk9VVBAJEhIKDlNYMTI2Ml9GQUlMVVJFEAoS",
- "EQoNUkFESU9fU1BJX0JVRxALEiAKHEZMQVNIX0NPUlJVUFRJT05fUkVDT1ZF",
- "UkFCTEUQDBIiCh5GTEFTSF9DT1JSVVBUSU9OX1VOUkVDT1ZFUkFCTEUQDSqA",
- "AwoPRXhjbHVkZWRNb2R1bGVzEhEKDUVYQ0xVREVEX05PTkUQABIPCgtNUVRU",
- "X0NPTkZJRxABEhEKDVNFUklBTF9DT05GSUcQAhITCg9FWFROT1RJRl9DT05G",
- "SUcQBBIXChNTVE9SRUZPUldBUkRfQ09ORklHEAgSFAoQUkFOR0VURVNUX0NP",
- "TkZJRxAQEhQKEFRFTEVNRVRSWV9DT05GSUcQIBIUChBDQU5ORURNU0dfQ09O",
- "RklHEEASEQoMQVVESU9fQ09ORklHEIABEhoKFVJFTU9URUhBUkRXQVJFX0NP",
- "TkZJRxCAAhIYChNORUlHSEJPUklORk9fQ09ORklHEIAEEhsKFkFNQklFTlRM",
- "SUdIVElOR19DT05GSUcQgAgSGwoWREVURUNUSU9OU0VOU09SX0NPTkZJRxCA",
- "EBIWChFQQVhDT1VOVEVSX0NPTkZJRxCAIBIVChBCTFVFVE9PVEhfQ09ORklH",
- "EIBAEhQKDk5FVFdPUktfQ09ORklHEICAAUJfChNjb20uZ2Vla3N2aWxsZS5t",
- "ZXNoQgpNZXNoUHJvdG9zWiJnaXRodWIuY29tL21lc2h0YXN0aWMvZ28vZ2Vu",
- "ZXJhdGVkqgIUTWVzaHRhc3RpYy5Qcm90b2J1ZnO6AgBiBnByb3RvMw=="));
+ "ZXhjbHVkZWRfbW9kdWxlcxgMIAEoDSIaCglIZWFydGJlYXQSDQoFbm9uY2UY",
+ "ASABKA0iVQoVTm9kZVJlbW90ZUhhcmR3YXJlUGluEhAKCG5vZGVfbnVtGAEg",
+ "ASgNEioKA3BpbhgCIAEoCzIdLm1lc2h0YXN0aWMuUmVtb3RlSGFyZHdhcmVQ",
+ "aW4iZQoOQ2h1bmtlZFBheWxvYWQSEgoKcGF5bG9hZF9pZBgBIAEoDRITCgtj",
+ "aHVua19jb3VudBgCIAEoDRITCgtjaHVua19pbmRleBgDIAEoDRIVCg1wYXls",
+ "b2FkX2NodW5rGAQgASgMIh8KDXJlc2VuZF9jaHVua3MSDgoGY2h1bmtzGAEg",
+ "AygNIqoBChZDaHVua2VkUGF5bG9hZFJlc3BvbnNlEhIKCnBheWxvYWRfaWQY",
+ "ASABKA0SGgoQcmVxdWVzdF90cmFuc2ZlchgCIAEoCEgAEhkKD2FjY2VwdF90",
+ "cmFuc2ZlchgDIAEoCEgAEjIKDXJlc2VuZF9jaHVua3MYBCABKAsyGS5tZXNo",
+ "dGFzdGljLnJlc2VuZF9jaHVua3NIAEIRCg9wYXlsb2FkX3ZhcmlhbnQqzxAK",
+ "DUhhcmR3YXJlTW9kZWwSCQoFVU5TRVQQABIMCghUTE9SQV9WMhABEgwKCFRM",
+ "T1JBX1YxEAISEgoOVExPUkFfVjJfMV8xUDYQAxIJCgVUQkVBTRAEEg8KC0hF",
+ "TFRFQ19WMl8wEAUSDgoKVEJFQU1fVjBQNxAGEgoKBlRfRUNITxAHEhAKDFRM",
+ "T1JBX1YxXzFQMxAIEgsKB1JBSzQ2MzEQCRIPCgtIRUxURUNfVjJfMRAKEg0K",
+ "CUhFTFRFQ19WMRALEhgKFExJTFlHT19UQkVBTV9TM19DT1JFEAwSDAoIUkFL",
+ "MTEyMDAQDRILCgdOQU5PX0cxEA4SEgoOVExPUkFfVjJfMV8xUDgQDxIPCgtU",
+ "TE9SQV9UM19TMxAQEhQKEE5BTk9fRzFfRVhQTE9SRVIQERIRCg1OQU5PX0cy",
+ "X1VMVFJBEBISDQoJTE9SQV9UWVBFEBMSCwoHV0lQSE9ORRAUEg4KCldJT19X",
+ "TTExMTAQFRILCgdSQUsyNTYwEBYSEwoPSEVMVEVDX0hSVV8zNjAxEBcSGgoW",
+ "SEVMVEVDX1dJUkVMRVNTX0JSSURHRRAYEg4KClNUQVRJT05fRzEQGRIMCghS",
+ "QUsxMTMxMBAaEhQKEFNFTlNFTE9SQV9SUDIwNDAQGxIQCgxTRU5TRUxPUkFf",
+ "UzMQHBINCglDQU5BUllPTkUQHRIPCgtSUDIwNDBfTE9SQRAeEg4KClNUQVRJ",
+ "T05fRzIQHxIRCg1MT1JBX1JFTEFZX1YxECASDgoKTlJGNTI4NDBESxAhEgcK",
+ "A1BQUhAiEg8KC0dFTklFQkxPQ0tTECMSEQoNTlJGNTJfVU5LTk9XThAkEg0K",
+ "CVBPUlREVUlOTxAlEg8KC0FORFJPSURfU0lNECYSCgoGRElZX1YxECcSFQoR",
+ "TlJGNTI4NDBfUENBMTAwNTkQKBIKCgZEUl9ERVYQKRILCgdNNVNUQUNLECoS",
+ "DQoJSEVMVEVDX1YzECsSEQoNSEVMVEVDX1dTTF9WMxAsEhMKD0JFVEFGUFZf",
+ "MjQwMF9UWBAtEhcKE0JFVEFGUFZfOTAwX05BTk9fVFgQLhIMCghSUElfUElD",
+ "TxAvEhsKF0hFTFRFQ19XSVJFTEVTU19UUkFDS0VSEDASGQoVSEVMVEVDX1dJ",
+ "UkVMRVNTX1BBUEVSEDESCgoGVF9ERUNLEDISDgoKVF9XQVRDSF9TMxAzEhEK",
+ "DVBJQ09NUFVURVJfUzMQNBIPCgtIRUxURUNfSFQ2MhA1EhIKDkVCWVRFX0VT",
+ "UDMyX1MzEDYSEQoNRVNQMzJfUzNfUElDTxA3Eg0KCUNIQVRURVJfMhA4Eh4K",
+ "GkhFTFRFQ19XSVJFTEVTU19QQVBFUl9WMV8wEDkSIAocSEVMVEVDX1dJUkVM",
+ "RVNTX1RSQUNLRVJfVjFfMBA6EgsKB1VOUEhPTkUQOxIMCghURF9MT1JBQxA8",
+ "EhMKD0NERUJZVEVfRU9SQV9TMxA9Eg8KC1RXQ19NRVNIX1Y0ED4SFgoSTlJG",
+ "NTJfUFJPTUlDUk9fRElZED8SHwobUkFESU9NQVNURVJfOTAwX0JBTkRJVF9O",
+ "QU5PEEASHAoYSEVMVEVDX0NBUFNVTEVfU0VOU09SX1YzEEESHQoZSEVMVEVD",
+ "X1ZJU0lPTl9NQVNURVJfVDE5MBBCEh0KGUhFTFRFQ19WSVNJT05fTUFTVEVS",
+ "X0UyMTMQQxIdChlIRUxURUNfVklTSU9OX01BU1RFUl9FMjkwEEQSGQoVSEVM",
+ "VEVDX01FU0hfTk9ERV9UMTE0EEUSFgoSU0VOU0VDQVBfSU5ESUNBVE9SEEYS",
+ "EwoPVFJBQ0tFUl9UMTAwMF9FEEcSCwoHUkFLMzE3MhBIEgoKBldJT19FNRBJ",
+ "EhoKFlJBRElPTUFTVEVSXzkwMF9CQU5ESVQQShITCg9NRTI1TFMwMV80WTEw",
+ "VEQQSxIYChRSUDIwNDBfRkVBVEhFUl9SRk05NRBMEhUKEU01U1RBQ0tfQ09S",
+ "RUJBU0lDEE0SEQoNTTVTVEFDS19DT1JFMhBOEg0KCVJQSV9QSUNPMhBPEhIK",
+ "Dk01U1RBQ0tfQ09SRVMzEFASEQoNU0VFRURfWElBT19TMxBREgsKB01TMjRT",
+ "RjEQUhIMCghUTE9SQV9DNhBTEg8KC1dJU01FU0hfVEFQEFQSDQoJUk9VVEFT",
+ "VElDEFUSDAoITUVTSF9UQUIQVhIMCghNRVNITElOSxBXEhIKDlhJQU9fTlJG",
+ "NTJfS0lUEFgSEAoMVEhJTktOT0RFX00xEFkSEAoMVEhJTktOT0RFX00yEFoS",
+ "DwoLVF9FVEhfRUxJVEUQWxIVChFIRUxURUNfU0VOU09SX0hVQhBcEhoKFlJF",
+ "U0VSVkVEX0ZSSUVEX0NISUNLRU4QXRIWChJIRUxURUNfTUVTSF9QT0NLRVQQ",
+ "XhIUChBTRUVFRF9TT0xBUl9OT0RFEF8SGAoUTk9NQURTVEFSX01FVEVPUl9Q",
+ "Uk8QYBINCglDUk9XUEFORUwQYRILCgdMSU5LXzMyEGISGAoUU0VFRURfV0lP",
+ "X1RSQUNLRVJfTDEQYxIdChlTRUVFRF9XSU9fVFJBQ0tFUl9MMV9FSU5LEGQS",
+ "FAoQUVdBTlRaX1RJTllfQVJNUxBlEg4KClRfREVDS19QUk8QZhIQCgxUX0xP",
+ "UkFfUEFHRVIQZxIdChlHQVQ1NjJfTUVTSF9UUklBTF9UUkFDS0VSEGgSDwoL",
+ "V0lTTUVTSF9UQUcQaRILCgdSQUszMzEyEGoSEAoMVEhJTktOT0RFX001EGsS",
+ "FQoRSEVMVEVDX01FU0hfU09MQVIQbBIPCgtUX0VDSE9fTElURRBtEg8KClBS",
+ "SVZBVEVfSFcQ/wEqLAoJQ29uc3RhbnRzEggKBFpFUk8QABIVChBEQVRBX1BB",
+ "WUxPQURfTEVOEOkBKrQCChFDcml0aWNhbEVycm9yQ29kZRIICgROT05FEAAS",
+ "DwoLVFhfV0FUQ0hET0cQARIUChBTTEVFUF9FTlRFUl9XQUlUEAISDAoITk9f",
+ "UkFESU8QAxIPCgtVTlNQRUNJRklFRBAEEhUKEVVCTE9YX1VOSVRfRkFJTEVE",
+ "EAUSDQoJTk9fQVhQMTkyEAYSGQoVSU5WQUxJRF9SQURJT19TRVRUSU5HEAcS",
+ "EwoPVFJBTlNNSVRfRkFJTEVEEAgSDAoIQlJPV05PVVQQCRISCg5TWDEyNjJf",
+ "RkFJTFVSRRAKEhEKDVJBRElPX1NQSV9CVUcQCxIgChxGTEFTSF9DT1JSVVBU",
+ "SU9OX1JFQ09WRVJBQkxFEAwSIgoeRkxBU0hfQ09SUlVQVElPTl9VTlJFQ09W",
+ "RVJBQkxFEA0qfwoPRmlybXdhcmVFZGl0aW9uEgsKB1ZBTklMTEEQABIRCg1T",
+ "TUFSVF9DSVRJWkVOEAESDgoKT1BFTl9TQVVDRRAQEgoKBkRFRkNPThAREg8K",
+ "C0JVUk5JTkdfTUFOEBISDgoKSEFNVkVOVElPThATEg8KC0RJWV9FRElUSU9O",
+ "EH8qgAMKD0V4Y2x1ZGVkTW9kdWxlcxIRCg1FWENMVURFRF9OT05FEAASDwoL",
+ "TVFUVF9DT05GSUcQARIRCg1TRVJJQUxfQ09ORklHEAISEwoPRVhUTk9USUZf",
+ "Q09ORklHEAQSFwoTU1RPUkVGT1JXQVJEX0NPTkZJRxAIEhQKEFJBTkdFVEVT",
+ "VF9DT05GSUcQEBIUChBURUxFTUVUUllfQ09ORklHECASFAoQQ0FOTkVETVNH",
+ "X0NPTkZJRxBAEhEKDEFVRElPX0NPTkZJRxCAARIaChVSRU1PVEVIQVJEV0FS",
+ "RV9DT05GSUcQgAISGAoTTkVJR0hCT1JJTkZPX0NPTkZJRxCABBIbChZBTUJJ",
+ "RU5UTElHSFRJTkdfQ09ORklHEIAIEhsKFkRFVEVDVElPTlNFTlNPUl9DT05G",
+ "SUcQgBASFgoRUEFYQ09VTlRFUl9DT05GSUcQgCASFQoQQkxVRVRPT1RIX0NP",
+ "TkZJRxCAQBIUCg5ORVRXT1JLX0NPTkZJRxCAgAFCXwoTY29tLmdlZWtzdmls",
+ "bGUubWVzaEIKTWVzaFByb3Rvc1oiZ2l0aHViLmNvbS9tZXNodGFzdGljL2dv",
+ "L2dlbmVyYXRlZKoCFE1lc2h0YXN0aWMuUHJvdG9idWZzugIAYgZwcm90bzM="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::Meshtastic.Protobufs.ChannelReflection.Descriptor, global::Meshtastic.Protobufs.ConfigReflection.Descriptor, global::Meshtastic.Protobufs.DeviceUiReflection.Descriptor, global::Meshtastic.Protobufs.ModuleConfigReflection.Descriptor, global::Meshtastic.Protobufs.PortnumsReflection.Descriptor, global::Meshtastic.Protobufs.TelemetryReflection.Descriptor, global::Meshtastic.Protobufs.XmodemReflection.Descriptor, },
- new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Meshtastic.Protobufs.HardwareModel), typeof(global::Meshtastic.Protobufs.Constants), typeof(global::Meshtastic.Protobufs.CriticalErrorCode), typeof(global::Meshtastic.Protobufs.ExcludedModules), }, null, new pbr::GeneratedClrTypeInfo[] {
+ new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Meshtastic.Protobufs.HardwareModel), typeof(global::Meshtastic.Protobufs.Constants), typeof(global::Meshtastic.Protobufs.CriticalErrorCode), typeof(global::Meshtastic.Protobufs.FirmwareEdition), typeof(global::Meshtastic.Protobufs.ExcludedModules), }, null, new pbr::GeneratedClrTypeInfo[] {
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.Position), global::Meshtastic.Protobufs.Position.Parser, new[]{ "LatitudeI", "LongitudeI", "Altitude", "Time", "LocationSource", "AltitudeSource", "Timestamp", "TimestampMillisAdjust", "AltitudeHae", "AltitudeGeoidalSeparation", "PDOP", "HDOP", "VDOP", "GpsAccuracy", "GroundSpeed", "GroundTrack", "FixQuality", "FixType", "SatsInView", "SensorId", "NextUpdate", "SeqNumber", "PrecisionBits" }, new[]{ "LatitudeI", "LongitudeI", "Altitude", "AltitudeHae", "AltitudeGeoidalSeparation", "GroundSpeed", "GroundTrack" }, new[]{ typeof(global::Meshtastic.Protobufs.Position.Types.LocSource), typeof(global::Meshtastic.Protobufs.Position.Types.AltSource) }, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.User), global::Meshtastic.Protobufs.User.Parser, new[]{ "Id", "LongName", "ShortName", "Macaddr", "HwModel", "IsLicensed", "Role", "PublicKey", "IsUnmessagable" }, new[]{ "IsUnmessagable" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.RouteDiscovery), global::Meshtastic.Protobufs.RouteDiscovery.Parser, new[]{ "Route", "SnrTowards", "RouteBack", "SnrBack" }, null, null, null, null),
@@ -255,9 +267,9 @@ static MeshReflection() {
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.KeyVerification), global::Meshtastic.Protobufs.KeyVerification.Parser, new[]{ "Nonce", "Hash1", "Hash2" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.Waypoint), global::Meshtastic.Protobufs.Waypoint.Parser, new[]{ "Id", "LatitudeI", "LongitudeI", "Expire", "LockedTo", "Name", "Description", "Icon" }, new[]{ "LatitudeI", "LongitudeI" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.MqttClientProxyMessage), global::Meshtastic.Protobufs.MqttClientProxyMessage.Parser, new[]{ "Topic", "Data", "Text", "Retained" }, new[]{ "PayloadVariant" }, null, null, null),
- new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.MeshPacket), global::Meshtastic.Protobufs.MeshPacket.Parser, new[]{ "From", "To", "Channel", "Decoded", "Encrypted", "Id", "RxTime", "RxSnr", "HopLimit", "WantAck", "Priority", "RxRssi", "Delayed", "ViaMqtt", "HopStart", "PublicKey", "PkiEncrypted", "NextHop", "RelayNode", "TxAfter" }, new[]{ "PayloadVariant" }, new[]{ typeof(global::Meshtastic.Protobufs.MeshPacket.Types.Priority), typeof(global::Meshtastic.Protobufs.MeshPacket.Types.Delayed) }, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.MeshPacket), global::Meshtastic.Protobufs.MeshPacket.Parser, new[]{ "From", "To", "Channel", "Decoded", "Encrypted", "Id", "RxTime", "RxSnr", "HopLimit", "WantAck", "Priority", "RxRssi", "Delayed", "ViaMqtt", "HopStart", "PublicKey", "PkiEncrypted", "NextHop", "RelayNode", "TxAfter", "TransportMechanism" }, new[]{ "PayloadVariant" }, new[]{ typeof(global::Meshtastic.Protobufs.MeshPacket.Types.Priority), typeof(global::Meshtastic.Protobufs.MeshPacket.Types.Delayed), typeof(global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism) }, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.NodeInfo), global::Meshtastic.Protobufs.NodeInfo.Parser, new[]{ "Num", "User", "Position", "Snr", "LastHeard", "DeviceMetrics", "Channel", "ViaMqtt", "HopsAway", "IsFavorite", "IsIgnored", "IsKeyManuallyVerified" }, new[]{ "HopsAway" }, null, null, null),
- new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.MyNodeInfo), global::Meshtastic.Protobufs.MyNodeInfo.Parser, new[]{ "MyNodeNum", "RebootCount", "MinAppVersion", "DeviceId", "PioEnv" }, null, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.MyNodeInfo), global::Meshtastic.Protobufs.MyNodeInfo.Parser, new[]{ "MyNodeNum", "RebootCount", "MinAppVersion", "DeviceId", "PioEnv", "FirmwareEdition", "NodedbCount" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.LogRecord), global::Meshtastic.Protobufs.LogRecord.Parser, new[]{ "Message", "Time", "Source", "Level" }, null, new[]{ typeof(global::Meshtastic.Protobufs.LogRecord.Types.Level) }, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.QueueStatus), global::Meshtastic.Protobufs.QueueStatus.Parser, new[]{ "Res", "Free", "Maxlen", "MeshPacketId" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.FromRadio), global::Meshtastic.Protobufs.FromRadio.Parser, new[]{ "Id", "Packet", "MyInfo", "NodeInfo", "Config", "LogRecord", "ConfigCompleteId", "Rebooted", "ModuleConfig", "Channel", "QueueStatus", "XmodemPacket", "Metadata", "MqttClientProxyMessage", "FileInfo", "ClientNotification", "DeviceuiConfig" }, new[]{ "PayloadVariant" }, null, null, null),
@@ -273,7 +285,7 @@ static MeshReflection() {
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.NeighborInfo), global::Meshtastic.Protobufs.NeighborInfo.Parser, new[]{ "NodeId", "LastSentById", "NodeBroadcastIntervalSecs", "Neighbors" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.Neighbor), global::Meshtastic.Protobufs.Neighbor.Parser, new[]{ "NodeId", "Snr", "LastRxTime", "NodeBroadcastIntervalSecs" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.DeviceMetadata), global::Meshtastic.Protobufs.DeviceMetadata.Parser, new[]{ "FirmwareVersion", "DeviceStateVersion", "CanShutdown", "HasWifi", "HasBluetooth", "HasEthernet", "Role", "PositionFlags", "HwModel", "HasRemoteHardware", "HasPKC", "ExcludedModules" }, null, null, null, null),
- new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.Heartbeat), global::Meshtastic.Protobufs.Heartbeat.Parser, null, null, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.Heartbeat), global::Meshtastic.Protobufs.Heartbeat.Parser, new[]{ "Nonce" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.NodeRemoteHardwarePin), global::Meshtastic.Protobufs.NodeRemoteHardwarePin.Parser, new[]{ "NodeNum", "Pin" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.ChunkedPayload), global::Meshtastic.Protobufs.ChunkedPayload.Parser, new[]{ "PayloadId", "ChunkCount", "ChunkIndex", "PayloadChunk" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Meshtastic.Protobufs.resend_chunks), global::Meshtastic.Protobufs.resend_chunks.Parser, new[]{ "Chunks" }, null, null, null, null),
@@ -853,6 +865,17 @@ public enum HardwareModel {
[pbr::OriginalName("THINKNODE_M5")] ThinknodeM5 = 107,
///
///
+ /// MeshSolar is an integrated power management and communication solution designed for outdoor low-power devices.
+ /// https://heltec.org/project/meshsolar/
+ ///
+ [pbr::OriginalName("HELTEC_MESH_SOLAR")] HeltecMeshSolar = 108,
+ ///
+ ///
+ /// Lilygo T-Echo Lite
+ ///
+ [pbr::OriginalName("T_ECHO_LITE")] TEchoLite = 109,
+ ///
+ ///
/// ------------------------------------------------------------------------------------------------------------------------------------------
/// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
/// ------------------------------------------------------------------------------------------------------------------------------------------
@@ -965,6 +988,49 @@ public enum CriticalErrorCode {
[pbr::OriginalName("FLASH_CORRUPTION_UNRECOVERABLE")] FlashCorruptionUnrecoverable = 13,
}
+ ///
+ ///
+ /// Enum to indicate to clients whether this firmware is a special firmware build, like an event.
+ /// The first 16 values are reserved for non-event special firmwares, like the Smart Citizen use case.
+ ///
+ public enum FirmwareEdition {
+ ///
+ ///
+ /// Vanilla firmware
+ ///
+ [pbr::OriginalName("VANILLA")] Vanilla = 0,
+ ///
+ ///
+ /// Firmware for use in the Smart Citizen environmental monitoring network
+ ///
+ [pbr::OriginalName("SMART_CITIZEN")] SmartCitizen = 1,
+ ///
+ ///
+ /// Open Sauce, the maker conference held yearly in CA
+ ///
+ [pbr::OriginalName("OPEN_SAUCE")] OpenSauce = 16,
+ ///
+ ///
+ /// DEFCON, the yearly hacker conference
+ ///
+ [pbr::OriginalName("DEFCON")] Defcon = 17,
+ ///
+ ///
+ /// Burning Man, the yearly hippie gathering in the desert
+ ///
+ [pbr::OriginalName("BURNING_MAN")] BurningMan = 18,
+ ///
+ ///
+ /// Hamvention, the Dayton amateur radio convention
+ ///
+ [pbr::OriginalName("HAMVENTION")] Hamvention = 19,
+ ///
+ ///
+ /// Placeholder for DIY and unofficial events
+ ///
+ [pbr::OriginalName("DIY_EDITION")] DiyEdition = 127,
+ }
+
///
///
/// Enum for modules excluded from a device's configuration.
@@ -5533,6 +5599,7 @@ public MeshPacket(MeshPacket other) : this() {
nextHop_ = other.nextHop_;
relayNode_ = other.relayNode_;
txAfter_ = other.txAfter_;
+ transportMechanism_ = other.transportMechanism_;
switch (other.PayloadVariantCase) {
case PayloadVariantOneofCase.Decoded:
Decoded = other.Decoded.Clone();
@@ -5923,6 +5990,22 @@ public uint TxAfter {
}
}
+ /// Field number for the "transport_mechanism" field.
+ public const int TransportMechanismFieldNumber = 21;
+ private global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism transportMechanism_ = global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism.TransportInternal;
+ ///
+ ///
+ /// Indicates which transport mechanism this packet arrived over
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism TransportMechanism {
+ get { return transportMechanism_; }
+ set {
+ transportMechanism_ = value;
+ }
+ }
+
private object payloadVariant_;
/// Enum of possible cases for the "payload_variant" oneof.
public enum PayloadVariantOneofCase {
@@ -5979,6 +6062,7 @@ public bool Equals(MeshPacket other) {
if (NextHop != other.NextHop) return false;
if (RelayNode != other.RelayNode) return false;
if (TxAfter != other.TxAfter) return false;
+ if (TransportMechanism != other.TransportMechanism) return false;
if (PayloadVariantCase != other.PayloadVariantCase) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -6007,6 +6091,7 @@ public override int GetHashCode() {
if (NextHop != 0) hash ^= NextHop.GetHashCode();
if (RelayNode != 0) hash ^= RelayNode.GetHashCode();
if (TxAfter != 0) hash ^= TxAfter.GetHashCode();
+ if (TransportMechanism != global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism.TransportInternal) hash ^= TransportMechanism.GetHashCode();
hash ^= (int) payloadVariantCase_;
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
@@ -6106,6 +6191,10 @@ public void WriteTo(pb::CodedOutputStream output) {
output.WriteRawTag(160, 1);
output.WriteUInt32(TxAfter);
}
+ if (TransportMechanism != global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism.TransportInternal) {
+ output.WriteRawTag(168, 1);
+ output.WriteEnum((int) TransportMechanism);
+ }
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -6196,6 +6285,10 @@ public void WriteTo(pb::CodedOutputStream output) {
output.WriteRawTag(160, 1);
output.WriteUInt32(TxAfter);
}
+ if (TransportMechanism != global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism.TransportInternal) {
+ output.WriteRawTag(168, 1);
+ output.WriteEnum((int) TransportMechanism);
+ }
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -6266,6 +6359,9 @@ public int CalculateSize() {
if (TxAfter != 0) {
size += 2 + pb::CodedOutputStream.ComputeUInt32Size(TxAfter);
}
+ if (TransportMechanism != global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism.TransportInternal) {
+ size += 2 + pb::CodedOutputStream.ComputeEnumSize((int) TransportMechanism);
+ }
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -6332,6 +6428,9 @@ public void MergeFrom(MeshPacket other) {
if (other.TxAfter != 0) {
TxAfter = other.TxAfter;
}
+ if (other.TransportMechanism != global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism.TransportInternal) {
+ TransportMechanism = other.TransportMechanism;
+ }
switch (other.PayloadVariantCase) {
case PayloadVariantOneofCase.Decoded:
if (Decoded == null) {
@@ -6448,6 +6547,10 @@ public void MergeFrom(pb::CodedInputStream input) {
TxAfter = input.ReadUInt32();
break;
}
+ case 168: {
+ TransportMechanism = (global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism) input.ReadEnum();
+ break;
+ }
}
}
#endif
@@ -6552,6 +6655,10 @@ public void MergeFrom(pb::CodedInputStream input) {
TxAfter = input.ReadUInt32();
break;
}
+ case 168: {
+ TransportMechanism = (global::Meshtastic.Protobufs.MeshPacket.Types.TransportMechanism) input.ReadEnum();
+ break;
+ }
}
}
}
@@ -6662,6 +6769,53 @@ public enum Delayed {
[pbr::OriginalName("DELAYED_DIRECT")] Direct = 2,
}
+ ///
+ ///
+ /// Enum to identify which transport mechanism this packet arrived over
+ ///
+ public enum TransportMechanism {
+ ///
+ ///
+ /// The default case is that the node generated a packet itself
+ ///
+ [pbr::OriginalName("TRANSPORT_INTERNAL")] TransportInternal = 0,
+ ///
+ ///
+ /// Arrived via the primary LoRa radio
+ ///
+ [pbr::OriginalName("TRANSPORT_LORA")] TransportLora = 1,
+ ///
+ ///
+ /// Arrived via a secondary LoRa radio
+ ///
+ [pbr::OriginalName("TRANSPORT_LORA_ALT1")] TransportLoraAlt1 = 2,
+ ///
+ ///
+ /// Arrived via a tertiary LoRa radio
+ ///
+ [pbr::OriginalName("TRANSPORT_LORA_ALT2")] TransportLoraAlt2 = 3,
+ ///
+ ///
+ /// Arrived via a quaternary LoRa radio
+ ///
+ [pbr::OriginalName("TRANSPORT_LORA_ALT3")] TransportLoraAlt3 = 4,
+ ///
+ ///
+ /// Arrived via an MQTT connection
+ ///
+ [pbr::OriginalName("TRANSPORT_MQTT")] TransportMqtt = 5,
+ ///
+ ///
+ /// Arrived via Multicast UDP
+ ///
+ [pbr::OriginalName("TRANSPORT_MULTICAST_UDP")] TransportMulticastUdp = 6,
+ ///
+ ///
+ /// Arrived via API connection
+ ///
+ [pbr::OriginalName("TRANSPORT_API")] TransportApi = 7,
+ }
+
}
#endregion
@@ -7435,6 +7589,8 @@ public MyNodeInfo(MyNodeInfo other) : this() {
minAppVersion_ = other.minAppVersion_;
deviceId_ = other.deviceId_;
pioEnv_ = other.pioEnv_;
+ firmwareEdition_ = other.firmwareEdition_;
+ nodedbCount_ = other.nodedbCount_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
@@ -7527,6 +7683,39 @@ public string PioEnv {
}
}
+ /// Field number for the "firmware_edition" field.
+ public const int FirmwareEditionFieldNumber = 14;
+ private global::Meshtastic.Protobufs.FirmwareEdition firmwareEdition_ = global::Meshtastic.Protobufs.FirmwareEdition.Vanilla;
+ ///
+ ///
+ /// The indicator for whether this device is running event firmware and which
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public global::Meshtastic.Protobufs.FirmwareEdition FirmwareEdition {
+ get { return firmwareEdition_; }
+ set {
+ firmwareEdition_ = value;
+ }
+ }
+
+ /// Field number for the "nodedb_count" field.
+ public const int NodedbCountFieldNumber = 15;
+ private uint nodedbCount_;
+ ///
+ ///
+ /// The number of nodes in the nodedb.
+ /// This is used by the phone to know how many NodeInfo packets to expect on want_config
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public uint NodedbCount {
+ get { return nodedbCount_; }
+ set {
+ nodedbCount_ = value;
+ }
+ }
+
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
@@ -7547,6 +7736,8 @@ public bool Equals(MyNodeInfo other) {
if (MinAppVersion != other.MinAppVersion) return false;
if (DeviceId != other.DeviceId) return false;
if (PioEnv != other.PioEnv) return false;
+ if (FirmwareEdition != other.FirmwareEdition) return false;
+ if (NodedbCount != other.NodedbCount) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -7559,6 +7750,8 @@ public override int GetHashCode() {
if (MinAppVersion != 0) hash ^= MinAppVersion.GetHashCode();
if (DeviceId.Length != 0) hash ^= DeviceId.GetHashCode();
if (PioEnv.Length != 0) hash ^= PioEnv.GetHashCode();
+ if (FirmwareEdition != global::Meshtastic.Protobufs.FirmwareEdition.Vanilla) hash ^= FirmwareEdition.GetHashCode();
+ if (NodedbCount != 0) hash ^= NodedbCount.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
@@ -7597,6 +7790,14 @@ public void WriteTo(pb::CodedOutputStream output) {
output.WriteRawTag(106);
output.WriteString(PioEnv);
}
+ if (FirmwareEdition != global::Meshtastic.Protobufs.FirmwareEdition.Vanilla) {
+ output.WriteRawTag(112);
+ output.WriteEnum((int) FirmwareEdition);
+ }
+ if (NodedbCount != 0) {
+ output.WriteRawTag(120);
+ output.WriteUInt32(NodedbCount);
+ }
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -7627,6 +7828,14 @@ public void WriteTo(pb::CodedOutputStream output) {
output.WriteRawTag(106);
output.WriteString(PioEnv);
}
+ if (FirmwareEdition != global::Meshtastic.Protobufs.FirmwareEdition.Vanilla) {
+ output.WriteRawTag(112);
+ output.WriteEnum((int) FirmwareEdition);
+ }
+ if (NodedbCount != 0) {
+ output.WriteRawTag(120);
+ output.WriteUInt32(NodedbCount);
+ }
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -7652,6 +7861,12 @@ public int CalculateSize() {
if (PioEnv.Length != 0) {
size += 1 + pb::CodedOutputStream.ComputeStringSize(PioEnv);
}
+ if (FirmwareEdition != global::Meshtastic.Protobufs.FirmwareEdition.Vanilla) {
+ size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) FirmwareEdition);
+ }
+ if (NodedbCount != 0) {
+ size += 1 + pb::CodedOutputStream.ComputeUInt32Size(NodedbCount);
+ }
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -7679,6 +7894,12 @@ public void MergeFrom(MyNodeInfo other) {
if (other.PioEnv.Length != 0) {
PioEnv = other.PioEnv;
}
+ if (other.FirmwareEdition != global::Meshtastic.Protobufs.FirmwareEdition.Vanilla) {
+ FirmwareEdition = other.FirmwareEdition;
+ }
+ if (other.NodedbCount != 0) {
+ NodedbCount = other.NodedbCount;
+ }
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
@@ -7718,6 +7939,14 @@ public void MergeFrom(pb::CodedInputStream input) {
PioEnv = input.ReadString();
break;
}
+ case 112: {
+ FirmwareEdition = (global::Meshtastic.Protobufs.FirmwareEdition) input.ReadEnum();
+ break;
+ }
+ case 120: {
+ NodedbCount = input.ReadUInt32();
+ break;
+ }
}
}
#endif
@@ -7757,6 +7986,14 @@ public void MergeFrom(pb::CodedInputStream input) {
PioEnv = input.ReadString();
break;
}
+ case 112: {
+ FirmwareEdition = (global::Meshtastic.Protobufs.FirmwareEdition) input.ReadEnum();
+ break;
+ }
+ case 120: {
+ NodedbCount = input.ReadUInt32();
+ break;
+ }
}
}
}
@@ -13789,6 +14026,7 @@ public Heartbeat() {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public Heartbeat(Heartbeat other) : this() {
+ nonce_ = other.nonce_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
@@ -13798,6 +14036,22 @@ public Heartbeat Clone() {
return new Heartbeat(this);
}
+ /// Field number for the "nonce" field.
+ public const int NonceFieldNumber = 1;
+ private uint nonce_;
+ ///
+ ///
+ /// The nonce of the heartbeat message
+ ///
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
+ [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
+ public uint Nonce {
+ get { return nonce_; }
+ set {
+ nonce_ = value;
+ }
+ }
+
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
@@ -13813,6 +14067,7 @@ public bool Equals(Heartbeat other) {
if (ReferenceEquals(other, this)) {
return true;
}
+ if (Nonce != other.Nonce) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -13820,6 +14075,7 @@ public bool Equals(Heartbeat other) {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
+ if (Nonce != 0) hash ^= Nonce.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
@@ -13838,6 +14094,10 @@ public void WriteTo(pb::CodedOutputStream output) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
+ if (Nonce != 0) {
+ output.WriteRawTag(8);
+ output.WriteUInt32(Nonce);
+ }
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -13848,6 +14108,10 @@ public void WriteTo(pb::CodedOutputStream output) {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
+ if (Nonce != 0) {
+ output.WriteRawTag(8);
+ output.WriteUInt32(Nonce);
+ }
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -13858,6 +14122,9 @@ public void WriteTo(pb::CodedOutputStream output) {
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
+ if (Nonce != 0) {
+ size += 1 + pb::CodedOutputStream.ComputeUInt32Size(Nonce);
+ }
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -13870,6 +14137,9 @@ public void MergeFrom(Heartbeat other) {
if (other == null) {
return;
}
+ if (other.Nonce != 0) {
+ Nonce = other.Nonce;
+ }
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
@@ -13889,6 +14159,10 @@ public void MergeFrom(pb::CodedInputStream input) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
+ case 8: {
+ Nonce = input.ReadUInt32();
+ break;
+ }
}
}
#endif
@@ -13908,6 +14182,10 @@ public void MergeFrom(pb::CodedInputStream input) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
+ case 8: {
+ Nonce = input.ReadUInt32();
+ break;
+ }
}
}
}
diff --git a/Meshtastic/Generated/ModuleConfig.cs b/Meshtastic/Generated/ModuleConfig.cs
index 6606faa..dd09d34 100644
--- a/Meshtastic/Generated/ModuleConfig.cs
+++ b/Meshtastic/Generated/ModuleConfig.cs
@@ -25,7 +25,7 @@ static ModuleConfigReflection() {
byte[] descriptorData = global::System.Convert.FromBase64String(
string.Concat(
"Ch5tZXNodGFzdGljL21vZHVsZV9jb25maWcucHJvdG8SCm1lc2h0YXN0aWMi",
- "4yUKDE1vZHVsZUNvbmZpZxIzCgRtcXR0GAEgASgLMiMubWVzaHRhc3RpYy5N",
+ "+iUKDE1vZHVsZUNvbmZpZxIzCgRtcXR0GAEgASgLMiMubWVzaHRhc3RpYy5N",
"b2R1bGVDb25maWcuTVFUVENvbmZpZ0gAEjcKBnNlcmlhbBgCIAEoCzIlLm1l",
"c2h0YXN0aWMuTW9kdWxlQ29uZmlnLlNlcmlhbENvbmZpZ0gAElQKFWV4dGVy",
"bmFsX25vdGlmaWNhdGlvbhgDIAEoCzIzLm1lc2h0YXN0aWMuTW9kdWxlQ29u",
@@ -79,7 +79,7 @@ static ModuleConfigReflection() {
"CgtDT0RFQzJfNzAwQhAIGnYKEFBheGNvdW50ZXJDb25maWcSDwoHZW5hYmxl",
"ZBgBIAEoCBIiChpwYXhjb3VudGVyX3VwZGF0ZV9pbnRlcnZhbBgCIAEoDRIW",
"Cg53aWZpX3RocmVzaG9sZBgDIAEoBRIVCg1ibGVfdGhyZXNob2xkGAQgASgF",
- "Gv0ECgxTZXJpYWxDb25maWcSDwoHZW5hYmxlZBgBIAEoCBIMCgRlY2hvGAIg",
+ "GowFCgxTZXJpYWxDb25maWcSDwoHZW5hYmxlZBgBIAEoCBIMCgRlY2hvGAIg",
"ASgIEgsKA3J4ZBgDIAEoDRILCgN0eGQYBCABKA0SPwoEYmF1ZBgFIAEoDjIx",
"Lm1lc2h0YXN0aWMuTW9kdWxlQ29uZmlnLlNlcmlhbENvbmZpZy5TZXJpYWxf",
"QmF1ZBIPCgd0aW1lb3V0GAYgASgNEj8KBG1vZGUYByABKA4yMS5tZXNodGFz",
@@ -90,55 +90,56 @@ static ModuleConfigReflection() {
"NDAwEAUSDQoJQkFVRF80ODAwEAYSDQoJQkFVRF85NjAwEAcSDgoKQkFVRF8x",
"OTIwMBAIEg4KCkJBVURfMzg0MDAQCRIOCgpCQVVEXzU3NjAwEAoSDwoLQkFV",
"RF8xMTUyMDAQCxIPCgtCQVVEXzIzMDQwMBAMEg8KC0JBVURfNDYwODAwEA0S",
- "DwoLQkFVRF81NzYwMDAQDhIPCgtCQVVEXzkyMTYwMBAPIm4KC1NlcmlhbF9N",
+ "DwoLQkFVRF81NzYwMDAQDhIPCgtCQVVEXzkyMTYwMBAPIn0KC1NlcmlhbF9N",
"b2RlEgsKB0RFRkFVTFQQABIKCgZTSU1QTEUQARIJCgVQUk9UTxACEgsKB1RF",
"WFRNU0cQAxIICgROTUVBEAQSCwoHQ0FMVE9QTxAFEggKBFdTODUQBhINCglW",
- "RV9ESVJFQ1QQBxrpAgoaRXh0ZXJuYWxOb3RpZmljYXRpb25Db25maWcSDwoH",
- "ZW5hYmxlZBgBIAEoCBIRCglvdXRwdXRfbXMYAiABKA0SDgoGb3V0cHV0GAMg",
- "ASgNEhQKDG91dHB1dF92aWJyYRgIIAEoDRIVCg1vdXRwdXRfYnV6emVyGAkg",
- "ASgNEg4KBmFjdGl2ZRgEIAEoCBIVCg1hbGVydF9tZXNzYWdlGAUgASgIEhsK",
- "E2FsZXJ0X21lc3NhZ2VfdmlicmEYCiABKAgSHAoUYWxlcnRfbWVzc2FnZV9i",
- "dXp6ZXIYCyABKAgSEgoKYWxlcnRfYmVsbBgGIAEoCBIYChBhbGVydF9iZWxs",
- "X3ZpYnJhGAwgASgIEhkKEWFsZXJ0X2JlbGxfYnV6emVyGA0gASgIEg8KB3Vz",
- "ZV9wd20YByABKAgSEwoLbmFnX3RpbWVvdXQYDiABKA0SGQoRdXNlX2kyc19h",
- "c19idXp6ZXIYDyABKAgalwEKElN0b3JlRm9yd2FyZENvbmZpZxIPCgdlbmFi",
- "bGVkGAEgASgIEhEKCWhlYXJ0YmVhdBgCIAEoCBIPCgdyZWNvcmRzGAMgASgN",
- "EhoKEmhpc3RvcnlfcmV0dXJuX21heBgEIAEoDRIdChVoaXN0b3J5X3JldHVy",
- "bl93aW5kb3cYBSABKA0SEQoJaXNfc2VydmVyGAYgASgIGkAKD1JhbmdlVGVz",
- "dENvbmZpZxIPCgdlbmFibGVkGAEgASgIEg4KBnNlbmRlchgCIAEoDRIMCgRz",
- "YXZlGAMgASgIGskDCg9UZWxlbWV0cnlDb25maWcSHgoWZGV2aWNlX3VwZGF0",
- "ZV9pbnRlcnZhbBgBIAEoDRIjChtlbnZpcm9ubWVudF91cGRhdGVfaW50ZXJ2",
- "YWwYAiABKA0SJwofZW52aXJvbm1lbnRfbWVhc3VyZW1lbnRfZW5hYmxlZBgD",
- "IAEoCBIiChplbnZpcm9ubWVudF9zY3JlZW5fZW5hYmxlZBgEIAEoCBImCh5l",
- "bnZpcm9ubWVudF9kaXNwbGF5X2ZhaHJlbmhlaXQYBSABKAgSGwoTYWlyX3F1",
- "YWxpdHlfZW5hYmxlZBgGIAEoCBIcChRhaXJfcXVhbGl0eV9pbnRlcnZhbBgH",
- "IAEoDRIhChlwb3dlcl9tZWFzdXJlbWVudF9lbmFibGVkGAggASgIEh0KFXBv",
- "d2VyX3VwZGF0ZV9pbnRlcnZhbBgJIAEoDRIcChRwb3dlcl9zY3JlZW5fZW5h",
- "YmxlZBgKIAEoCBIiChpoZWFsdGhfbWVhc3VyZW1lbnRfZW5hYmxlZBgLIAEo",
- "CBIeChZoZWFsdGhfdXBkYXRlX2ludGVydmFsGAwgASgNEh0KFWhlYWx0aF9z",
- "Y3JlZW5fZW5hYmxlZBgNIAEoCBrWBAoTQ2FubmVkTWVzc2FnZUNvbmZpZxIX",
- "Cg9yb3RhcnkxX2VuYWJsZWQYASABKAgSGQoRaW5wdXRicm9rZXJfcGluX2EY",
- "AiABKA0SGQoRaW5wdXRicm9rZXJfcGluX2IYAyABKA0SHQoVaW5wdXRicm9r",
- "ZXJfcGluX3ByZXNzGAQgASgNElkKFGlucHV0YnJva2VyX2V2ZW50X2N3GAUg",
- "ASgOMjsubWVzaHRhc3RpYy5Nb2R1bGVDb25maWcuQ2FubmVkTWVzc2FnZUNv",
- "bmZpZy5JbnB1dEV2ZW50Q2hhchJaChVpbnB1dGJyb2tlcl9ldmVudF9jY3cY",
- "BiABKA4yOy5tZXNodGFzdGljLk1vZHVsZUNvbmZpZy5DYW5uZWRNZXNzYWdl",
- "Q29uZmlnLklucHV0RXZlbnRDaGFyElwKF2lucHV0YnJva2VyX2V2ZW50X3By",
- "ZXNzGAcgASgOMjsubWVzaHRhc3RpYy5Nb2R1bGVDb25maWcuQ2FubmVkTWVz",
- "c2FnZUNvbmZpZy5JbnB1dEV2ZW50Q2hhchIXCg91cGRvd24xX2VuYWJsZWQY",
- "CCABKAgSDwoHZW5hYmxlZBgJIAEoCBIaChJhbGxvd19pbnB1dF9zb3VyY2UY",
- "CiABKAkSEQoJc2VuZF9iZWxsGAsgASgIImMKDklucHV0RXZlbnRDaGFyEggK",
- "BE5PTkUQABIGCgJVUBAREggKBERPV04QEhIICgRMRUZUEBMSCQoFUklHSFQQ",
- "FBIKCgZTRUxFQ1QQChIICgRCQUNLEBsSCgoGQ0FOQ0VMEBgaZQoVQW1iaWVu",
- "dExpZ2h0aW5nQ29uZmlnEhEKCWxlZF9zdGF0ZRgBIAEoCBIPCgdjdXJyZW50",
- "GAIgASgNEgsKA3JlZBgDIAEoDRINCgVncmVlbhgEIAEoDRIMCgRibHVlGAUg",
- "ASgNQhEKD3BheWxvYWRfdmFyaWFudCJkChFSZW1vdGVIYXJkd2FyZVBpbhIQ",
- "CghncGlvX3BpbhgBIAEoDRIMCgRuYW1lGAIgASgJEi8KBHR5cGUYAyABKA4y",
- "IS5tZXNodGFzdGljLlJlbW90ZUhhcmR3YXJlUGluVHlwZSpJChVSZW1vdGVI",
- "YXJkd2FyZVBpblR5cGUSCwoHVU5LTk9XThAAEhAKDERJR0lUQUxfUkVBRBAB",
- "EhEKDURJR0lUQUxfV1JJVEUQAkJnChNjb20uZ2Vla3N2aWxsZS5tZXNoQhJN",
- "b2R1bGVDb25maWdQcm90b3NaImdpdGh1Yi5jb20vbWVzaHRhc3RpYy9nby9n",
- "ZW5lcmF0ZWSqAhRNZXNodGFzdGljLlByb3RvYnVmc7oCAGIGcHJvdG8z"));
+ "RV9ESVJFQ1QQBxINCglNU19DT05GSUcQCBrpAgoaRXh0ZXJuYWxOb3RpZmlj",
+ "YXRpb25Db25maWcSDwoHZW5hYmxlZBgBIAEoCBIRCglvdXRwdXRfbXMYAiAB",
+ "KA0SDgoGb3V0cHV0GAMgASgNEhQKDG91dHB1dF92aWJyYRgIIAEoDRIVCg1v",
+ "dXRwdXRfYnV6emVyGAkgASgNEg4KBmFjdGl2ZRgEIAEoCBIVCg1hbGVydF9t",
+ "ZXNzYWdlGAUgASgIEhsKE2FsZXJ0X21lc3NhZ2VfdmlicmEYCiABKAgSHAoU",
+ "YWxlcnRfbWVzc2FnZV9idXp6ZXIYCyABKAgSEgoKYWxlcnRfYmVsbBgGIAEo",
+ "CBIYChBhbGVydF9iZWxsX3ZpYnJhGAwgASgIEhkKEWFsZXJ0X2JlbGxfYnV6",
+ "emVyGA0gASgIEg8KB3VzZV9wd20YByABKAgSEwoLbmFnX3RpbWVvdXQYDiAB",
+ "KA0SGQoRdXNlX2kyc19hc19idXp6ZXIYDyABKAgalwEKElN0b3JlRm9yd2Fy",
+ "ZENvbmZpZxIPCgdlbmFibGVkGAEgASgIEhEKCWhlYXJ0YmVhdBgCIAEoCBIP",
+ "CgdyZWNvcmRzGAMgASgNEhoKEmhpc3RvcnlfcmV0dXJuX21heBgEIAEoDRId",
+ "ChVoaXN0b3J5X3JldHVybl93aW5kb3cYBSABKA0SEQoJaXNfc2VydmVyGAYg",
+ "ASgIGkAKD1JhbmdlVGVzdENvbmZpZxIPCgdlbmFibGVkGAEgASgIEg4KBnNl",
+ "bmRlchgCIAEoDRIMCgRzYXZlGAMgASgIGskDCg9UZWxlbWV0cnlDb25maWcS",
+ "HgoWZGV2aWNlX3VwZGF0ZV9pbnRlcnZhbBgBIAEoDRIjChtlbnZpcm9ubWVu",
+ "dF91cGRhdGVfaW50ZXJ2YWwYAiABKA0SJwofZW52aXJvbm1lbnRfbWVhc3Vy",
+ "ZW1lbnRfZW5hYmxlZBgDIAEoCBIiChplbnZpcm9ubWVudF9zY3JlZW5fZW5h",
+ "YmxlZBgEIAEoCBImCh5lbnZpcm9ubWVudF9kaXNwbGF5X2ZhaHJlbmhlaXQY",
+ "BSABKAgSGwoTYWlyX3F1YWxpdHlfZW5hYmxlZBgGIAEoCBIcChRhaXJfcXVh",
+ "bGl0eV9pbnRlcnZhbBgHIAEoDRIhChlwb3dlcl9tZWFzdXJlbWVudF9lbmFi",
+ "bGVkGAggASgIEh0KFXBvd2VyX3VwZGF0ZV9pbnRlcnZhbBgJIAEoDRIcChRw",
+ "b3dlcl9zY3JlZW5fZW5hYmxlZBgKIAEoCBIiChpoZWFsdGhfbWVhc3VyZW1l",
+ "bnRfZW5hYmxlZBgLIAEoCBIeChZoZWFsdGhfdXBkYXRlX2ludGVydmFsGAwg",
+ "ASgNEh0KFWhlYWx0aF9zY3JlZW5fZW5hYmxlZBgNIAEoCBreBAoTQ2FubmVk",
+ "TWVzc2FnZUNvbmZpZxIXCg9yb3RhcnkxX2VuYWJsZWQYASABKAgSGQoRaW5w",
+ "dXRicm9rZXJfcGluX2EYAiABKA0SGQoRaW5wdXRicm9rZXJfcGluX2IYAyAB",
+ "KA0SHQoVaW5wdXRicm9rZXJfcGluX3ByZXNzGAQgASgNElkKFGlucHV0YnJv",
+ "a2VyX2V2ZW50X2N3GAUgASgOMjsubWVzaHRhc3RpYy5Nb2R1bGVDb25maWcu",
+ "Q2FubmVkTWVzc2FnZUNvbmZpZy5JbnB1dEV2ZW50Q2hhchJaChVpbnB1dGJy",
+ "b2tlcl9ldmVudF9jY3cYBiABKA4yOy5tZXNodGFzdGljLk1vZHVsZUNvbmZp",
+ "Zy5DYW5uZWRNZXNzYWdlQ29uZmlnLklucHV0RXZlbnRDaGFyElwKF2lucHV0",
+ "YnJva2VyX2V2ZW50X3ByZXNzGAcgASgOMjsubWVzaHRhc3RpYy5Nb2R1bGVD",
+ "b25maWcuQ2FubmVkTWVzc2FnZUNvbmZpZy5JbnB1dEV2ZW50Q2hhchIXCg91",
+ "cGRvd24xX2VuYWJsZWQYCCABKAgSEwoHZW5hYmxlZBgJIAEoCEICGAESHgoS",
+ "YWxsb3dfaW5wdXRfc291cmNlGAogASgJQgIYARIRCglzZW5kX2JlbGwYCyAB",
+ "KAgiYwoOSW5wdXRFdmVudENoYXISCAoETk9ORRAAEgYKAlVQEBESCAoERE9X",
+ "ThASEggKBExFRlQQExIJCgVSSUdIVBAUEgoKBlNFTEVDVBAKEggKBEJBQ0sQ",
+ "GxIKCgZDQU5DRUwQGBplChVBbWJpZW50TGlnaHRpbmdDb25maWcSEQoJbGVk",
+ "X3N0YXRlGAEgASgIEg8KB2N1cnJlbnQYAiABKA0SCwoDcmVkGAMgASgNEg0K",
+ "BWdyZWVuGAQgASgNEgwKBGJsdWUYBSABKA1CEQoPcGF5bG9hZF92YXJpYW50",
+ "ImQKEVJlbW90ZUhhcmR3YXJlUGluEhAKCGdwaW9fcGluGAEgASgNEgwKBG5h",
+ "bWUYAiABKAkSLwoEdHlwZRgDIAEoDjIhLm1lc2h0YXN0aWMuUmVtb3RlSGFy",
+ "ZHdhcmVQaW5UeXBlKkkKFVJlbW90ZUhhcmR3YXJlUGluVHlwZRILCgdVTktO",
+ "T1dOEAASEAoMRElHSVRBTF9SRUFEEAESEQoNRElHSVRBTF9XUklURRACQmcK",
+ "E2NvbS5nZWVrc3ZpbGxlLm1lc2hCEk1vZHVsZUNvbmZpZ1Byb3Rvc1oiZ2l0",
+ "aHViLmNvbS9tZXNodGFzdGljL2dvL2dlbmVyYXRlZKoCFE1lc2h0YXN0aWMu",
+ "UHJvdG9idWZzugIAYgZwcm90bzM="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { },
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Meshtastic.Protobufs.RemoteHardwarePinType), }, null, new pbr::GeneratedClrTypeInfo[] {
@@ -4493,6 +4494,11 @@ public enum Serial_Mode {
/// https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable
///
[pbr::OriginalName("VE_DIRECT")] VeDirect = 7,
+ ///
+ ///Used to configure and view some parameters of MeshSolar.
+ ///https://heltec.org/project/meshsolar/
+ ///
+ [pbr::OriginalName("MS_CONFIG")] MsConfig = 8,
}
}
@@ -6886,6 +6892,7 @@ public bool Updown1Enabled {
///
/// Enable/disable CannedMessageModule.
///
+ [global::System.ObsoleteAttribute]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool Enabled {
@@ -6903,6 +6910,7 @@ public bool Enabled {
/// Input event origin accepted by the canned message module.
/// Can be e.g. "rotEnc1", "upDownEnc1", "scanAndSelect", "cardkb", "serialkb", or keyword "_any"
///
+ [global::System.ObsoleteAttribute]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public string AllowInputSource {
diff --git a/Meshtastic/Meshtastic.csproj b/Meshtastic/Meshtastic.csproj
index 7a8f5d9..19d3003 100644
--- a/Meshtastic/Meshtastic.csproj
+++ b/Meshtastic/Meshtastic.csproj
@@ -33,6 +33,7 @@
+
diff --git a/protobufs b/protobufs
index fa02e14..5dd723f 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit fa02e14d8d01850336eaea0e9552aef4f08f0a40
+Subproject commit 5dd723fe6f33a8613ec81acf5e15be26365c7cce