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;
}
}

102 changes: 102 additions & 0 deletions HealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#nullable enable
using System;
using System.Text.Json;

namespace InstDotNet;

/// <summary>
/// Health check service that tracks application health status
/// </summary>
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;

/// <summary>
/// Initialize the health check service
/// </summary>
public static void Initialize()
{
_startTime = DateTime.UtcNow;
}

/// <summary>
/// Update the last processing time
/// </summary>
public static void UpdateLastProcessTime()
{
_lastUpdateTime = DateTime.UtcNow;
}

/// <summary>
/// Update beacon count
/// </summary>
public static void UpdateBeaconCount(int count)
{
_beaconCount = count;
}

/// <summary>
/// Increment total nodes processed
/// </summary>
public static void IncrementNodesProcessed(int count = 1)
{
_totalNodesProcessed += count;
}

/// <summary>
/// Get current health status
/// </summary>
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
};
}

/// <summary>
/// Get health status as JSON string
/// </summary>
public static string GetStatusJson()
{
var status = GetStatus();
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
return JsonSerializer.Serialize(status, options);
}
}

/// <summary>
/// Health status model
/// </summary>
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;
}

Loading
Loading