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/HealthCheck.cs b/HealthCheck.cs
new file mode 100644
index 0000000..153e6b7
--- /dev/null
+++ b/HealthCheck.cs
@@ -0,0 +1,102 @@
+#nullable enable
+using System;
+using System.Text.Json;
+
+namespace InstDotNet;
+
+///
+/// Health check service that tracks application health status
+///
+public static class HealthCheck
+{
+ private static DateTime _lastUpdateTime = DateTime.MinValue;
+ private static int _beaconCount = 0;
+ private static int _totalNodesProcessed = 0;
+ private static DateTime _startTime = DateTime.UtcNow;
+
+ ///
+ /// Initialize the health check service
+ ///
+ public static void Initialize()
+ {
+ _startTime = DateTime.UtcNow;
+ }
+
+ ///
+ /// Update the last processing time
+ ///
+ public static void UpdateLastProcessTime()
+ {
+ _lastUpdateTime = DateTime.UtcNow;
+ }
+
+ ///
+ /// Update beacon count
+ ///
+ public static void UpdateBeaconCount(int count)
+ {
+ _beaconCount = count;
+ }
+
+ ///
+ /// Increment total nodes processed
+ ///
+ public static void IncrementNodesProcessed(int count = 1)
+ {
+ _totalNodesProcessed += count;
+ }
+
+ ///
+ /// Get current health status
+ ///
+ public static HealthStatus GetStatus()
+ {
+ var mqttConnected = MQTTControl.IsConnected();
+ var uptime = DateTime.UtcNow - _startTime;
+ var timeSinceLastUpdate = _lastUpdateTime == DateTime.MinValue
+ ? TimeSpan.Zero
+ : DateTime.UtcNow - _lastUpdateTime;
+
+ return new HealthStatus
+ {
+ Status = mqttConnected && timeSinceLastUpdate.TotalSeconds < 60 ? "healthy" : "degraded",
+ MqttConnected = mqttConnected,
+ LastUpdateTime = _lastUpdateTime == DateTime.MinValue ? null : _lastUpdateTime,
+ TimeSinceLastUpdate = timeSinceLastUpdate.TotalSeconds,
+ BeaconCount = _beaconCount,
+ TotalNodesProcessed = _totalNodesProcessed,
+ UptimeSeconds = uptime.TotalSeconds,
+ Version = VersionInfo.FullVersion
+ };
+ }
+
+ ///
+ /// Get health status as JSON string
+ ///
+ public static string GetStatusJson()
+ {
+ var status = GetStatus();
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+ return JsonSerializer.Serialize(status, options);
+ }
+}
+
+///
+/// Health status model
+///
+public class HealthStatus
+{
+ public string Status { get; set; } = "unknown";
+ public bool MqttConnected { get; set; }
+ public DateTime? LastUpdateTime { get; set; }
+ public double TimeSinceLastUpdate { get; set; }
+ public int BeaconCount { get; set; }
+ public int TotalNodesProcessed { get; set; }
+ public double UptimeSeconds { get; set; }
+ public string Version { get; set; } = string.Empty;
+}
+
diff --git a/HealthCheckServer.cs b/HealthCheckServer.cs
new file mode 100644
index 0000000..f73ad2f
--- /dev/null
+++ b/HealthCheckServer.cs
@@ -0,0 +1,128 @@
+#nullable enable
+using System;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace InstDotNet;
+
+///
+/// HTTP server for health check endpoint
+///
+public static class HealthCheckServer
+{
+ private static HttpListener? _listener;
+ private static Task? _serverTask;
+ private static CancellationTokenSource? _cts;
+ private static int _port = 8080;
+
+ ///
+ /// Start the health check HTTP server
+ ///
+ public static void Start(int port = 8080)
+ {
+ _port = port;
+ _cts = new CancellationTokenSource();
+
+ _listener = new HttpListener();
+ _listener.Prefixes.Add($"http://+:{port}/");
+
+ try
+ {
+ _listener.Start();
+ Console.WriteLine($"Health check server started on port {port}");
+
+ _serverTask = Task.Run(async () => await ListenAsync(_cts.Token), _cts.Token);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to start health check server on port {port}: {ex.Message}");
+ throw;
+ }
+ }
+
+ ///
+ /// Stop the health check server
+ ///
+ public static void Stop()
+ {
+ _cts?.Cancel();
+ _listener?.Stop();
+ _listener?.Close();
+ Console.WriteLine("Health check server stopped");
+ }
+
+ private static async Task ListenAsync(CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested && _listener != null && _listener.IsListening)
+ {
+ try
+ {
+ var context = await _listener.GetContextAsync().ConfigureAwait(false);
+ _ = Task.Run(() => HandleRequest(context), cancellationToken);
+ }
+ catch (ObjectDisposedException)
+ {
+ // Listener was closed, exit gracefully
+ break;
+ }
+ catch (HttpListenerException ex)
+ {
+ Console.WriteLine($"Health check server error: {ex.Message}");
+ if (!_listener.IsListening)
+ break;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Unexpected error in health check server: {ex.Message}");
+ }
+ }
+ }
+
+ private static void HandleRequest(HttpListenerContext context)
+ {
+ try
+ {
+ var request = context.Request;
+ var response = context.Response;
+
+ // Only handle GET requests to /health
+ if (request.HttpMethod == "GET" && request.Url?.AbsolutePath == "/health")
+ {
+ var status = HealthCheck.GetStatus();
+ var json = HealthCheck.GetStatusJson();
+
+ // Set status code based on health
+ response.StatusCode = status.Status == "healthy" ? 200 : 503;
+ response.ContentType = "application/json";
+ response.ContentEncoding = Encoding.UTF8;
+
+ var buffer = Encoding.UTF8.GetBytes(json);
+ response.ContentLength64 = buffer.Length;
+ response.OutputStream.Write(buffer, 0, buffer.Length);
+ response.OutputStream.Close();
+ }
+ else
+ {
+ // 404 for other paths
+ response.StatusCode = 404;
+ response.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error handling health check request: {ex.Message}");
+ try
+ {
+ context.Response.StatusCode = 500;
+ context.Response.Close();
+ }
+ catch
+ {
+ // Ignore errors when closing response
+ }
+ }
+ }
+}
+
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..c3a19e5 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))
@@ -182,5 +202,13 @@ public static void ReceiveMessage(string message)
OnMessageReceived?.Invoke(message);
}
+ ///
+ /// Check if MQTT client is connected
+ ///
+ public static bool IsConnected()
+ {
+ return client != null && client.IsConnected;
+ }
+
}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index 8fb45d1..ea7c4a3 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,46 @@ 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);
+
+ // Initialize health check
+ HealthCheck.Initialize();
+
+ // Start health check server
+ int healthCheckPort = _config?.Application.HealthCheckPort ?? 8080;
+ try
+ {
+ HealthCheckServer.Start(healthCheckPort);
+ Console.WriteLine($"Health check endpoint available at http://localhost:{healthCheckPort}/health");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to start health check server on port {healthCheckPort}, continuing without health endpoint: {ex.Message}");
+ }
+
+ await MQTTControl.Initialise(cts, _config);
UWBManager.Initialise();
//Try loading from Python-generated file if it exists
@@ -39,8 +77,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 () =>
@@ -66,6 +104,8 @@ static async Task Main()
Console.WriteLine("Press Ctrl+C to exit…");
try { await Task.Delay(Timeout.Infinite, cts.Token); } catch { }
+ Console.WriteLine("Shutting down...");
+ HealthCheckServer.Stop();
await MQTTControl.DisconnectAsync();
}
}
diff --git a/UWBManager.cs b/UWBManager.cs
index 567a0bf..e0b45b0 100644
--- a/UWBManager.cs
+++ b/UWBManager.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Numerics;
using System.Text.Json;
-
+using InstDotNet;
public class UWBManager
{
@@ -60,6 +60,7 @@ private static void UpdateUwbs()
Console.WriteLine("UpdateUwbs: already updating — skipping re-entrant call.");
return;
}
+ isUpdating = true;
UWB2GPSConverter.ConvertUWBToPositions(network, true);
@@ -81,6 +82,11 @@ private static void UpdateUwbs()
}
sendNetwork = new UWB2GPSConverter.Network(sendUwbsList.ToArray());
+ // Update health check
+ HealthCheck.UpdateLastProcessTime();
+ HealthCheck.UpdateBeaconCount(sendNetwork.uwbs.Length);
+ HealthCheck.IncrementNodesProcessed(network.uwbs.Length);
+
SendNetwork(sendNetwork);
isUpdating = false;
}
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": []
+}
+