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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions AppConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#nullable enable
using Microsoft.Extensions.Configuration;

namespace InstDotNet;

/// <summary>
/// Application configuration model loaded from appsettings.json
/// </summary>
public class AppConfig
{
public MqttConfig MQTT { get; set; } = new();
public ApplicationConfig Application { get; set; } = new();
public AlgorithmConfig Algorithm { get; set; } = new();
public List<BeaconConfig> Beacons { get; set; } = new();

/// <summary>
/// Load configuration from appsettings.json and environment variables
/// </summary>
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;
}

/// <summary>
/// Resolves placeholders in configuration strings (e.g., {$BoardID})
/// </summary>
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; }
}

150 changes: 150 additions & 0 deletions HardwareId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;

namespace InstDotNet;

/// <summary>
/// Provides unique hardware identifiers for the system.
/// </summary>
public static class HardwareId
{
private static string? _cachedId;

/// <summary>
/// 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)
/// </summary>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="prefix">Optional prefix to prepend to the hardware ID (e.g., "UwbManager")</param>
/// <returns>A sanitized MQTT client ID string, truncated to 128 characters if necessary</returns>
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;
}
}

11 changes: 11 additions & 0 deletions InstDotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@

<ItemGroup>
<PackageReference Include="MQTTnet" Version="5.0.1.1416" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
</ItemGroup>

<!-- Copy appsettings.json to output directory -->
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
72 changes: 46 additions & 26 deletions MQTTControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,33 +19,57 @@
private static string _receiveMessageTopic;
private static string _sendMessageTopic;
private static int _timeoutInSeconds;

private static int _keepAlivePeriodSeconds;

public static System.Action<string> OnMessageReceived;

private static IMqttClient client;
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)

Check warning on line 30 in MQTTControl.cs

View workflow job for this annotation

GitHub Actions / Build linux-arm64 Artifacts

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
_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();
Expand Down Expand Up @@ -90,7 +108,9 @@
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))
Expand Down
Loading
Loading