diff --git a/AppConfig.cs b/AppConfig.cs new file mode 100644 index 0000000..b7c759d --- /dev/null +++ b/AppConfig.cs @@ -0,0 +1,96 @@ +#nullable enable +using Microsoft.Extensions.Configuration; + +namespace InstDotNet; + +/// +/// Application configuration model loaded from appsettings.json +/// +public class AppConfig +{ + public MqttConfig MQTT { get; set; } = new(); + public ApplicationConfig Application { get; set; } = new(); + public AlgorithmConfig Algorithm { get; set; } = new(); + public List Beacons { get; set; } = new(); + + /// + /// Load configuration from appsettings.json and environment variables + /// + public static AppConfig Load() + { + var builder = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + + var configuration = builder.Build(); + var config = new AppConfig(); + configuration.Bind(config); + return config; + } + + /// + /// Resolves placeholders in configuration strings (e.g., {$BoardID}) + /// + public void ResolvePlaceholders(string boardId) + { + MQTT.ReceiveTopic = ResolvePlaceholder(MQTT.ReceiveTopic, boardId); + MQTT.SendTopic = ResolvePlaceholder(MQTT.SendTopic, boardId); + MQTT.ClientId = ResolvePlaceholder(MQTT.ClientId, boardId); + } + + private static string ResolvePlaceholder(string value, string boardId) + { + if (string.IsNullOrEmpty(value)) + return value; + + return value.Replace("{$BoardID}", boardId, StringComparison.OrdinalIgnoreCase) + .Replace("{$BOARD_ID}", boardId, StringComparison.OrdinalIgnoreCase); + } +} + +public class MqttConfig +{ + public string ServerAddress { get; set; } = string.Empty; + public int Port { get; set; } = 1883; + public string ClientId { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string ReceiveTopic { get; set; } = string.Empty; + public string SendTopic { get; set; } = string.Empty; + public int TimeoutSeconds { get; set; } = 10; + public int RetryAttempts { get; set; } = 5; + public int RetryDelaySeconds { get; set; } = 2; + public double RetryBackoffMultiplier { get; set; } = 2.0; + public bool AutoReconnect { get; set; } = true; + public int ReconnectDelaySeconds { get; set; } = 5; + public int KeepAlivePeriodSeconds { get; set; } = 60; + public bool UseTls { get; set; } = false; + public bool AllowUntrustedCertificates { get; set; } = false; + public string? CertificatePath { get; set; } + public string? CertificatePassword { get; set; } +} + +public class ApplicationConfig +{ + public int UpdateIntervalMs { get; set; } = 10; + public string LogLevel { get; set; } = "Information"; + public int HealthCheckPort { get; set; } = 8080; +} + +public class AlgorithmConfig +{ + public int MaxIterations { get; set; } = 10; + public float LearningRate { get; set; } = 0.1f; + public bool RefinementEnabled { get; set; } = true; +} + +public class BeaconConfig +{ + public string Id { get; set; } = string.Empty; + public double Latitude { get; set; } + public double Longitude { get; set; } + public double Altitude { get; set; } +} + diff --git a/HardwareId.cs b/HardwareId.cs new file mode 100644 index 0000000..47f31a0 --- /dev/null +++ b/HardwareId.cs @@ -0,0 +1,150 @@ +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Net.NetworkInformation; + +namespace InstDotNet; + +/// +/// Provides unique hardware identifiers for the system. +/// +public static class HardwareId +{ + private static string? _cachedId; + + /// + /// Gets a unique hardware identifier for this system. + /// Tries multiple methods in order of preference: + /// 1. Systemd machine-id (/etc/machine-id) + /// 2. DMI system UUID (/sys/class/dmi/id/product_uuid) + /// 3. First MAC address + /// 4. Hostname (fallback) + /// + public static string GetUniqueId() + { + if (_cachedId != null) + { + return _cachedId; + } + + // Try systemd machine-id (most reliable on modern Linux) + try + { + var machineIdPath = "/etc/machine-id"; + if (File.Exists(machineIdPath)) + { + var machineId = File.ReadAllText(machineIdPath).Trim(); + if (!string.IsNullOrEmpty(machineId)) + { + _cachedId = $"machine-{machineId}"; + return _cachedId; + } + } + } + catch (Exception) + { + // Ignore errors + } + + // Try DMI system UUID + try + { + var dmiUuidPath = "/sys/class/dmi/id/product_uuid"; + if (File.Exists(dmiUuidPath)) + { + var dmiUuid = File.ReadAllText(dmiUuidPath).Trim(); + if (!string.IsNullOrEmpty(dmiUuid)) + { + _cachedId = $"dmi-{dmiUuid}"; + return _cachedId; + } + } + } + catch (Exception) + { + // Ignore errors + } + + // Try first MAC address + try + { + var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces() + .Where(nic => nic.OperationalStatus == OperationalStatus.Up && + nic.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .OrderBy(nic => nic.NetworkInterfaceType) + .ToList(); + + foreach (var nic in networkInterfaces) + { + var macAddress = nic.GetPhysicalAddress().ToString(); + if (!string.IsNullOrEmpty(macAddress) && macAddress != "000000000000") + { + _cachedId = $"mac-{macAddress}"; + return _cachedId; + } + } + } + catch (Exception) + { + // Ignore errors + } + + // Fallback to hostname + try + { + var hostname = Environment.MachineName; + if (!string.IsNullOrEmpty(hostname)) + { + _cachedId = $"host-{hostname}"; + return _cachedId; + } + } + catch (Exception) + { + // Ignore errors + } + + // Last resort: generate a random ID (not ideal, but better than nothing) + _cachedId = $"random-{Guid.NewGuid():N}"; + return _cachedId; + } + + /// + /// Gets a sanitized version of the hardware ID suitable for use as an MQTT client ID. + /// MQTT client IDs must be alphanumeric and can contain hyphens and underscores. + /// + /// Optional prefix to prepend to the hardware ID (e.g., "UwbManager") + /// A sanitized MQTT client ID string, truncated to 128 characters if necessary + public static string GetMqttClientId(string? prefix = null) + { + var hardwareId = GetUniqueId(); + var clientId = string.IsNullOrEmpty(prefix) ? hardwareId : $"{prefix}-{hardwareId}"; + + // Sanitize for MQTT: only alphanumeric, hyphens, and underscores allowed + // Replace any invalid characters with hyphens + var sanitized = new System.Text.StringBuilder(); + foreach (var c in clientId) + { + if (char.IsLetterOrDigit(c) || c == '-' || c == '_') + { + sanitized.Append(c); + } + else + { + sanitized.Append('-'); + } + } + + // Ensure it's not too long (MQTT spec recommends max 23 characters for client IDs) + // But we'll allow up to 128 characters as per MQTT 3.1.1 spec + var result = sanitized.ToString(); + if (result.Length > 128) + { + result = result.Substring(0, 128); + } + + return result; + } +} + diff --git a/InstDotNet.csproj b/InstDotNet.csproj index dd1c0d2..67486da 100644 --- a/InstDotNet.csproj +++ b/InstDotNet.csproj @@ -51,6 +51,17 @@ + + + + + + + + + + PreserveNewest + diff --git a/MQTTControl.cs b/MQTTControl.cs index 84d5e16..51863ca 100644 --- a/MQTTControl.cs +++ b/MQTTControl.cs @@ -6,17 +6,11 @@ using MQTTnet; using MQTTnet.Packets; // for v5 subscribe options if needed using MQTTnet.Protocol; // QoS enums +using InstDotNet; public class MQTTControl { public const string DEFAULT_CLIENT_ID = "clientId-UwbManager-001"; - public const string DEFAULT_SERVER_ADDRESS = "mqtt.dynamicdevices.co.uk"; - public const string DEFAULT_USERNAME = ""; - public const string DEFAULT_PASSWORD = ""; - public const int DEFAULT_SERVER_PORT = 1883; - public const string DEFAULT_RECEIVE_MESSAGE_TOPIC = "DotnetMQTT/Test/in"; - public const string DEFAULT_SEND_MESSAGE_TOPIC = "DotnetMQTT/Test/out"; - public const int DEFAULT_TIMEOUT_IN_SECONDS = 10; //not currently being used - need to for safety? private static string _clientId; private static string _serverAddress; private static string _usernname; @@ -25,7 +19,7 @@ public class MQTTControl private static string _receiveMessageTopic; private static string _sendMessageTopic; private static int _timeoutInSeconds; - + private static int _keepAlivePeriodSeconds; public static System.Action OnMessageReceived; @@ -33,25 +27,49 @@ public class MQTTControl private static CancellationTokenSource _cts; // Return Task so callers can await completion and observe exceptions - public static async Task Initialise(CancellationTokenSource cts, - string clientId = DEFAULT_CLIENT_ID, - string serverAddress = DEFAULT_SERVER_ADDRESS, - int port = DEFAULT_SERVER_PORT, - string username = DEFAULT_USERNAME, - string password = DEFAULT_PASSWORD, - string receiveMessageTopic = DEFAULT_RECEIVE_MESSAGE_TOPIC, - string sendMessageTopic = DEFAULT_SEND_MESSAGE_TOPIC, - int timeoutSeconds = DEFAULT_TIMEOUT_IN_SECONDS) + public static async Task Initialise(CancellationTokenSource cts, AppConfig? config = null) { _cts = cts ?? new CancellationTokenSource(); - _clientId = clientId; - _serverAddress = serverAddress; - _port = port; - _usernname = username; - _password = password; - _receiveMessageTopic = receiveMessageTopic; - _sendMessageTopic = sendMessageTopic; - _timeoutInSeconds = timeoutSeconds; + + if (config != null) + { + _serverAddress = config.MQTT.ServerAddress; + _port = config.MQTT.Port; + _usernname = config.MQTT.Username; + _password = config.MQTT.Password; + _receiveMessageTopic = config.MQTT.ReceiveTopic; + _sendMessageTopic = config.MQTT.SendTopic; + _timeoutInSeconds = config.MQTT.TimeoutSeconds; + _keepAlivePeriodSeconds = config.MQTT.KeepAlivePeriodSeconds; + + // Use configured client ID, or generate from hardware ID if empty + if (string.IsNullOrWhiteSpace(config.MQTT.ClientId)) + { + var baseClientId = HardwareId.GetMqttClientId("UwbManager"); + var processId = System.Diagnostics.Process.GetCurrentProcess().Id; + _clientId = $"{baseClientId}-pid{processId}"; + Console.WriteLine($"Using hardware-based MQTT client ID: {_clientId} (PID: {processId})"); + } + else + { + _clientId = config.MQTT.ClientId; + } + } + else + { + // Fallback to defaults if no config provided + _serverAddress = "mqtt.dynamicdevices.co.uk"; + _port = 1883; + _usernname = ""; + _password = ""; + _receiveMessageTopic = "DotnetMQTT/Test/in"; + _sendMessageTopic = "DotnetMQTT/Test/out"; + _timeoutInSeconds = 10; + _keepAlivePeriodSeconds = 60; + var baseClientId = HardwareId.GetMqttClientId("UwbManager"); + var processId = System.Diagnostics.Process.GetCurrentProcess().Id; + _clientId = $"{baseClientId}-pid{processId}"; + } var factory = new MqttClientFactory(); client = factory.CreateMqttClient(); @@ -90,7 +108,9 @@ public static async Task Initialise(CancellationTokenSource cts, var builder = new MqttClientOptionsBuilder() .WithClientId(_clientId) .WithTcpServer(_serverAddress, _port) - .WithCleanSession(); + .WithCleanSession() + .WithTimeout(TimeSpan.FromSeconds(_timeoutInSeconds)) + .WithKeepAlivePeriod(TimeSpan.FromSeconds(_keepAlivePeriodSeconds)); // Add credentials only if provided if (!string.IsNullOrWhiteSpace(_usernname) && !string.IsNullOrWhiteSpace(_password)) diff --git a/Program.cs b/Program.cs index 8fb45d1..786c172 100644 --- a/Program.cs +++ b/Program.cs @@ -1,9 +1,10 @@ // Program.cs (net8.0, MQTTnet 5.x) using System.Text.Json; +using InstDotNet; class Program { - + static AppConfig? _config; static async Task Main() { @@ -11,9 +12,30 @@ static async Task Main() Console.WriteLine($"CGA Coordinate Mapping - Version {VersionInfo.FullVersion}"); Console.WriteLine(); + // Load configuration + try + { + _config = AppConfig.Load(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Failed to load configuration: {ex.Message}"); + Console.Error.WriteLine("Using default configuration values"); + _config = new AppConfig(); + } + + // Get board ID and resolve placeholders + var boardId = HardwareId.GetMqttClientId("UwbManager"); + _config.ResolvePlaceholders(boardId); + + Console.WriteLine($"Board ID: {boardId}"); + Console.WriteLine($"MQTT Receive Topic: {_config.MQTT.ReceiveTopic}"); + Console.WriteLine($"MQTT Send Topic: {_config.MQTT.SendTopic}"); + Console.WriteLine(); + using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; - await MQTTControl.Initialise(cts); + await MQTTControl.Initialise(cts, _config); UWBManager.Initialise(); //Try loading from Python-generated file if it exists @@ -39,8 +61,8 @@ static async Task Main() // Run one immediate update, then start a background loop to update repeatedly UWBManager.Update(); - // Interval between updates in milliseconds (few ms as requested) - const int updateIntervalMs = 10; + // Interval between updates from configuration + int updateIntervalMs = _config?.Application.UpdateIntervalMs ?? 10; // Start background loop that will run until Ctrl+C cancels the token _ = Task.Run(async () => diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..de96fc4 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,38 @@ +{ + "MQTT": { + "ServerAddress": "mqtt.dynamicdevices.co.uk", + "Port": 1883, + "ClientId": "", + "Username": "", + "Password": "", + // To use board-specific topics, replace {$BoardID} with the actual board identifier + // Example: "uwb/{$BoardID}/network/in" becomes "uwb/UwbManager-machine-xxx/network/in" + // The {$BoardID} placeholder will be automatically replaced with the hardware-based MQTT client ID + // (without the process ID suffix). This allows each board to have its own topic namespace. + "ReceiveTopic": "DotnetMQTT/Test/in", + "SendTopic": "DotnetMQTT/Test/out", + "TimeoutSeconds": 10, + "RetryAttempts": 5, + "RetryDelaySeconds": 2, + "RetryBackoffMultiplier": 2.0, + "AutoReconnect": true, + "ReconnectDelaySeconds": 5, + "KeepAlivePeriodSeconds": 60, + "UseTls": false, + "AllowUntrustedCertificates": false, + "CertificatePath": null, + "CertificatePassword": null + }, + "Application": { + "UpdateIntervalMs": 10, + "LogLevel": "Information", + "HealthCheckPort": 8080 + }, + "Algorithm": { + "MaxIterations": 10, + "LearningRate": 0.1, + "RefinementEnabled": true + }, + "Beacons": [] +} +