From 4c30e479d3b6acabb316337f045f2fbc8df9e8e2 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 14 Aug 2025 05:59:05 -0500 Subject: [PATCH 01/15] E2E testing wip --- Directory.Packages.props | 1 + Meshtastic.Cli/DeviceConnectionContext.cs | 12 +- Meshtastic.IntegrationTest/GlobalSetup.cs | 32 ++ .../Meshtastic.IntegrationTest.csproj | 25 ++ Meshtastic.IntegrationTest/TestCategories.cs | 7 + .../ConnectedDeviceTests/TextMessageTests.cs | 140 ++++++++ .../Tests/IntegrationTestBase.cs | 8 + Meshtastic.IntegrationTest/Usings.cs | 8 + .../Utilities/ReleaseZipServiceTests.cs | 22 +- .../Persistance/VirtualStore.cs | 2 + Meshtastic.sln | 6 + Meshtastic/Discovery/DeviceDiscovery.cs | 312 ++++++++++++++++++ Meshtastic/Discovery/MeshtasticDevice.cs | 56 ++++ Meshtastic/Meshtastic.csproj | 1 + 14 files changed, 612 insertions(+), 20 deletions(-) create mode 100644 Meshtastic.IntegrationTest/GlobalSetup.cs create mode 100644 Meshtastic.IntegrationTest/Meshtastic.IntegrationTest.csproj create mode 100644 Meshtastic.IntegrationTest/TestCategories.cs create mode 100644 Meshtastic.IntegrationTest/Tests/ConnectedDeviceTests/TextMessageTests.cs create mode 100644 Meshtastic.IntegrationTest/Tests/IntegrationTestBase.cs create mode 100644 Meshtastic.IntegrationTest/Usings.cs create mode 100644 Meshtastic/Discovery/DeviceDiscovery.cs create mode 100644 Meshtastic/Discovery/MeshtasticDevice.cs 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/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/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/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/Meshtastic.csproj b/Meshtastic/Meshtastic.csproj index 7a8f5d9..19d3003 100644 --- a/Meshtastic/Meshtastic.csproj +++ b/Meshtastic/Meshtastic.csproj @@ -33,6 +33,7 @@ + From a096a4a0a9c72e55f4230d8f297bef4691ed3a11 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 14 Aug 2025 06:08:11 -0500 Subject: [PATCH 02/15] XEdDSA signing --- Meshtastic.Test/Crypto/XEdDSASigningTests.cs | 311 ++++++++++++++++++ Meshtastic/Crypto/XEdDSASigning.cs | 166 ++++++++++ .../NodeInfoMessageFactory.cs | 185 +++++++++++ 3 files changed, 662 insertions(+) create mode 100644 Meshtastic.Test/Crypto/XEdDSASigningTests.cs create mode 100644 Meshtastic/Crypto/XEdDSASigning.cs create mode 100644 Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs 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/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs new file mode 100644 index 0000000..057ce6b --- /dev/null +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -0,0 +1,166 @@ +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; + +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(); + var privateKey = ((Ed25519PrivateKeyParameters)keyPair.Private).GetEncoded(); + var publicKey = ((Ed25519PublicKeyParameters)keyPair.Public).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 + edPublicKey[31] &= 0x7F; + + return edPublicKey; + } + + /// + /// 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..8a940b7 --- /dev/null +++ b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs @@ -0,0 +1,185 @@ +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 ex) + { + // Log error but don't fail packet creation + Console.WriteLine($"Failed to sign NodeInfo packet: {ex.Message}"); + } + } + + 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]), + PublicKey = publicKey != null ? ByteString.CopyFrom(publicKey) : null + }; + + 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; + + Console.WriteLine($"Generated XEdDSA signature: {Convert.ToHexString(signature)}"); + } + + 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]; + random.NextBytes(privateKey); + return privateKey; + } + + private static uint GeneratePacketId() + { + var bytes = new byte[4]; + new SecureRandom().NextBytes(bytes); + return BitConverter.ToUInt32(bytes, 0); + } +} From ef4220f2daa34642821e230721e833d896da8953 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:08:16 -0500 Subject: [PATCH 03/15] Update protos --- Meshtastic.IntegrationTest/README.md | 81 +++++ Meshtastic/Generated/Config.cs | 113 +++---- Meshtastic/Generated/Mesh.cs | 470 +++++++++++++++++++++------ Meshtastic/Generated/ModuleConfig.cs | 106 +++--- protobufs | 2 +- 5 files changed, 570 insertions(+), 202 deletions(-) create mode 100644 Meshtastic.IntegrationTest/README.md 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/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/protobufs b/protobufs index fa02e14..5dd723f 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit fa02e14d8d01850336eaea0e9552aef4f08f0a40 +Subproject commit 5dd723fe6f33a8613ec81acf5e15be26365c7cce From c45300f2f6bc82babc6e06beefc8069766a24b50 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:18:21 -0500 Subject: [PATCH 04/15] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Crypto/XEdDSASigning.cs | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 057ce6b..56dd9dc 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -79,7 +79,39 @@ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) // Clear the sign bit (bit 7 of the last byte) as per Ed25519 specification edPublicKey[31] &= 0x7F; - + // 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 + BigInteger p = Ed25519FieldElement.Q; + + // 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[] edPublicKey = new byte[32]; + // Copy yBytes into edPublicKey (little-endian) + for (int i = 0; i < yBytes.Length && i < 32; i++) + { + edPublicKey[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) + edPublicKey[31] &= 0x7F; return edPublicKey; } From 17db27ffcff0e71b59e8f095f7d5521b31a8237b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:19:18 -0500 Subject: [PATCH 05/15] Update Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs index 8a940b7..82a9acd 100644 --- a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs +++ b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs @@ -172,7 +172,8 @@ private static void AddXEdDSASignature(MeshPacket meshPacket, DeviceStateContain // For testing, we'll generate a mock private key var random = new Random(); var privateKey = new byte[32]; - random.NextBytes(privateKey); + var privateKey = new byte[32]; + new SecureRandom().NextBytes(privateKey); return privateKey; } From 91676ef355550b719282338516b56d3b8ce6fa92 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:19:44 -0500 Subject: [PATCH 06/15] Update Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs index 82a9acd..f8affc1 100644 --- a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs +++ b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs @@ -60,7 +60,7 @@ public static MeshPacket CreateNodeInfoMessage( catch (Exception ex) { // Log error but don't fail packet creation - Console.WriteLine($"Failed to sign NodeInfo packet: {ex.Message}"); + // Swallow exception: failed to sign NodeInfo packet, but don't fail packet creation } } From 73eb1ff08204edbf1a761139c69a255a58d4f0d8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:19:57 -0500 Subject: [PATCH 07/15] Update Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs index f8affc1..16cfbfa 100644 --- a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs +++ b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs @@ -161,7 +161,6 @@ private static void AddXEdDSASignature(MeshPacket meshPacket, DeviceStateContain // meshPacket.Decoded.XeddsaSignature = ByteString.CopyFrom(signature); // meshPacket.Decoded.HasXeddsaSignature = true; - Console.WriteLine($"Generated XEdDSA signature: {Convert.ToHexString(signature)}"); } private static byte[]? GetDevicePrivateKey(DeviceStateContainer deviceStateContainer) From ebf1eacef3dfded5ac8c0dbbecc21160f9e3d648 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:20:05 -0500 Subject: [PATCH 08/15] Update Meshtastic/Crypto/XEdDSASigning.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Crypto/XEdDSASigning.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 56dd9dc..71284be 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -51,7 +51,21 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25 var keyPair = keyPairGen.GenerateKeyPair(); var privateKey = ((Ed25519PrivateKeyParameters)keyPair.Private).GetEncoded(); var publicKey = ((Ed25519PublicKeyParameters)keyPair.Public).GetEncoded(); - + // 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); } From db99b013f29c361526ad0ae489417a2055d8fd7c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:31:54 -0500 Subject: [PATCH 09/15] Update Meshtastic/Crypto/XEdDSASigning.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Crypto/XEdDSASigning.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 71284be..a4a2e1e 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -50,7 +50,6 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25 var keyPair = keyPairGen.GenerateKeyPair(); var privateKey = ((Ed25519PrivateKeyParameters)keyPair.Private).GetEncoded(); - var publicKey = ((Ed25519PublicKeyParameters)keyPair.Public).GetEncoded(); // Proper XEdDSA key derivation with domain separation // Ed25519 seed = SHA-512("XEdDSA" || x25519PrivateKey)[0..31] byte[] domain = Encoding.ASCII.GetBytes("XEdDSA"); From 3e1ad7e64eecf8bd17033b9313b4fa31ff789b75 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:31:59 -0500 Subject: [PATCH 10/15] Update Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs index 16cfbfa..9d2decd 100644 --- a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs +++ b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs @@ -171,7 +171,6 @@ private static void AddXEdDSASignature(MeshPacket meshPacket, DeviceStateContain // For testing, we'll generate a mock private key var random = new Random(); var privateKey = new byte[32]; - var privateKey = new byte[32]; new SecureRandom().NextBytes(privateKey); return privateKey; } From 5efd62af442df212ddb052dec060f32b91a14a1b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:32:11 -0500 Subject: [PATCH 11/15] Update Meshtastic/Crypto/XEdDSASigning.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Crypto/XEdDSASigning.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index a4a2e1e..1b296fe 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -97,7 +97,8 @@ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) // See: https://tools.ietf.org/html/rfc7748#section-5 // Curve25519 prime: 2^255 - 19 - BigInteger p = Ed25519FieldElement.Q; + // 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]; From 1c82344ae0073bd4963336f21f6e56b1c04966e2 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:32:37 -0500 Subject: [PATCH 12/15] Update Meshtastic/Crypto/XEdDSASigning.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Crypto/XEdDSASigning.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 1b296fe..50f10a0 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -91,7 +91,6 @@ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) var edPublicKey = ((Ed25519PublicKeyParameters)keyPair.Public).GetEncoded(); // Clear the sign bit (bit 7 of the last byte) as per Ed25519 specification - edPublicKey[31] &= 0x7F; // 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 From 0a939b2f35434771579cd3eb7539d978f4bf24ef Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:37:40 -0500 Subject: [PATCH 13/15] Update Meshtastic/Crypto/XEdDSASigning.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Crypto/XEdDSASigning.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 50f10a0..98dc941 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -49,7 +49,6 @@ public static (byte[] edPrivateKey, byte[] edPublicKey) GenerateEdDSAKeysFromX25 keyPairGen.Init(new KeyGenerationParameters(secureRandom, 256)); var keyPair = keyPairGen.GenerateKeyPair(); - var privateKey = ((Ed25519PrivateKeyParameters)keyPair.Private).GetEncoded(); // Proper XEdDSA key derivation with domain separation // Ed25519 seed = SHA-512("XEdDSA" || x25519PrivateKey)[0..31] byte[] domain = Encoding.ASCII.GetBytes("XEdDSA"); From 683e01996302cfbc7d4f3f9d513ced6a79fc2e78 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 06:56:43 -0500 Subject: [PATCH 14/15] Usings --- Meshtastic/Crypto/XEdDSASigning.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 98dc941..9d3b4b2 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -4,6 +4,8 @@ using Org.BouncyCastle.Security; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto; +using System.Numerics; +using System.Text; namespace Meshtastic.Crypto; From 33f1eb888bdb3afda2be7444a4788017fca0fd50 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 17 Aug 2025 07:33:54 -0500 Subject: [PATCH 15/15] Update --- Meshtastic/Crypto/XEdDSASigning.cs | 10 +- .../NodeInfoMessageFactory.cs | 129 +++++++++++++++++- 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Crypto/XEdDSASigning.cs b/Meshtastic/Crypto/XEdDSASigning.cs index 9d3b4b2..a94ec17 100644 --- a/Meshtastic/Crypto/XEdDSASigning.cs +++ b/Meshtastic/Crypto/XEdDSASigning.cs @@ -4,7 +4,7 @@ using Org.BouncyCastle.Security; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto; -using System.Numerics; +using Org.BouncyCastle.Math; using System.Text; namespace Meshtastic.Crypto; @@ -116,17 +116,17 @@ public static byte[] ConvertX25519PublicKeyToEd25519(byte[] x25519PublicKey) // Encode y as 32-byte little-endian byte[] yBytes = y.ToByteArrayUnsigned(); - byte[] edPublicKey = new byte[32]; + byte[] edPublicKeyResult = new byte[32]; // Copy yBytes into edPublicKey (little-endian) for (int i = 0; i < yBytes.Length && i < 32; i++) { - edPublicKey[i] = yBytes[yBytes.Length - 1 - 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) - edPublicKey[31] &= 0x7F; - return edPublicKey; + edPublicKeyResult[31] &= 0x7F; + return edPublicKeyResult; } /// diff --git a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs index 9d2decd..e4cad69 100644 --- a/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs +++ b/Meshtastic/Data/MessageFactories/NodeInfoMessageFactory.cs @@ -57,7 +57,7 @@ public static MeshPacket CreateNodeInfoMessage( { AddXEdDSASignature(meshPacket, deviceStateContainer, useShortHash); } - catch (Exception ex) + catch (Exception) { // Log error but don't fail packet creation // Swallow exception: failed to sign NodeInfo packet, but don't fail packet creation @@ -90,10 +90,14 @@ public static User CreateTestUser( Id = id, HwModel = hardwareModel, IsUnmessagable = false, - Macaddr = ByteString.CopyFrom([0xFF, 0xAA, 0x88, 0x99, 0x55, 0x66]), - PublicKey = publicKey != null ? ByteString.CopyFrom(publicKey) : null + // Macaddr = ByteString.CopyFrom([0xFF, 0xAA, 0x88, 0x99, 0x55, 0x66]), // Obsolete field }; + if (publicKey != null) + { + user.PublicKey = ByteString.CopyFrom(publicKey); + } + return user; } @@ -175,6 +179,59 @@ private static void AddXEdDSASignature(MeshPacket meshPacket, DeviceStateContain 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]; @@ -182,3 +239,69 @@ private static uint GeneratePacketId() 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; +}