Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/dotnet-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ jobs:
- name: Publish client
run: >
dotnet publish $env:Client_Project
-c Release -f net10.0-windows -r win-x64 --self-contained true
-c Release -f net10.0-windows -r win-x64 --self-contained false
-o $env:Client_Publish_Dir

- name: Publish service
run: >
dotnet publish $env:Service_Project
-c Release -f net10.0-windows -r win-x64 --self-contained true
-c Release -f net10.0-windows -r win-x64 --self-contained false
-o $env:Service_Publish_Dir

- name: Build installer
Expand Down
30 changes: 28 additions & 2 deletions src/StatusLightChecker.Contracts/Protos/settings.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ package settings;
service SettingsService {
// Get current color configuration
rpc GetColorConfig (ColorConfigRequest) returns (ColorConfigResponse);

// Update color configuration
rpc UpdateColorConfig (UpdateColorConfigRequest) returns (ColorConfigResponse);

// Subscribe to color config changes
rpc StreamColorConfigUpdates (ColorConfigRequest) returns (stream ColorConfigResponse);

// Get serial port configuration
rpc GetSerialPortConfig (SerialPortConfigRequest) returns (SerialPortConfigResponse);

// Update serial port configuration
rpc UpdateSerialPortConfig (UpdateSerialPortConfigRequest) returns (SerialPortConfigResponse);
}

message ColorConfigRequest {
Expand Down Expand Up @@ -45,4 +51,24 @@ message ColorConfigResponse {
int64 lastUpdatedUnixMs = 2;
bool success = 3;
string errorMessage = 4;
}

message SerialPortConfig {
string comPort = 1;
int32 baudRate = 2;
}

message SerialPortConfigRequest {
string clientId = 1;
}

message SerialPortConfigResponse {
SerialPortConfig config = 1;
bool success = 2;
string errorMessage = 3;
}

message UpdateSerialPortConfigRequest {
string clientId = 1;
SerialPortConfig config = 2;
}
121 changes: 118 additions & 3 deletions src/StatusLightChecker.Core/Services/ColorConfigurationService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using StatusLightChecker.Core.Models;
using System.Text.Json;
Expand Down Expand Up @@ -116,13 +117,127 @@ private async Task SaveConfigurationInternalAsync(ColorConfiguration configurati
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

using var command = new SqliteCommand(@"
INSERT OR REPLACE INTO ColorConfiguration (Id, ConfigurationJson, LastUpdated)
INSERT OR REPLACE INTO ColorConfiguration (Id, ConfigurationJson, LastUpdated)
VALUES (1, @json, @timestamp)
", connection);

command.Parameters.AddWithValue("@json", json);
command.Parameters.AddWithValue("@timestamp", timestamp);


await command.ExecuteNonQueryAsync();
}
}

public class SerialPortConfigurationService : ISerialPortConfigurationService
{
private const string SettingsKey = "SerialPort";

private readonly ILogger<SerialPortConfigurationService> _logger;
private readonly string _connectionString;
private SerialPortConfiguration _currentConfig;
private readonly SemaphoreSlim _semaphore = new(1, 1);

public SerialPortConfigurationService(
ILogger<SerialPortConfigurationService> logger,
string connectionString,
IConfiguration configuration)
{
_logger = logger;
_connectionString = connectionString;

var comPort = configuration["SerialPort:ComPort"] ?? "COM3";
var baudRate = int.TryParse(configuration["SerialPort:BaudRate"], out var br) ? br : 115200;
_currentConfig = new SerialPortConfiguration { ComPort = comPort, BaudRate = baudRate };

EnsureTableAsync().Wait();
LoadConfigurationAsync().Wait();
}

private async Task EnsureTableAsync()
{
using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();

using var command = new SqliteCommand(@"
CREATE TABLE IF NOT EXISTS Settings (
Key TEXT PRIMARY KEY,
Value TEXT NOT NULL,
LastUpdated INTEGER NOT NULL
);", connection);
await command.ExecuteNonQueryAsync();
}

private async Task LoadConfigurationAsync()
{
await _semaphore.WaitAsync();
try
{
using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();

using var command = new SqliteCommand(
"SELECT Value FROM Settings WHERE Key = @key", connection);
command.Parameters.AddWithValue("@key", SettingsKey);

var result = await command.ExecuteScalarAsync();
if (result is string json)
{
var config = JsonSerializer.Deserialize<SerialPortConfiguration>(json);
if (config != null)
{
_currentConfig = config;
_logger.LogInformation("Loaded serial port configuration from database");
}
}
else
{
await SaveInternalAsync(_currentConfig);
_logger.LogInformation("Saved default serial port configuration to database");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading serial port configuration");
}
finally
{
_semaphore.Release();
}
}

public SerialPortConfiguration GetCurrentConfiguration() => _currentConfig;

public async Task UpdateConfigurationAsync(SerialPortConfiguration configuration)
{
await _semaphore.WaitAsync();
try
{
await SaveInternalAsync(configuration);
_currentConfig = configuration;
_logger.LogInformation("Serial port configuration updated: {Port} @ {Baud}",
configuration.ComPort, configuration.BaudRate);
}
finally
{
_semaphore.Release();
}
}

private async Task SaveInternalAsync(SerialPortConfiguration configuration)
{
using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();

var json = JsonSerializer.Serialize(configuration);
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

using var command = new SqliteCommand(@"
INSERT OR REPLACE INTO Settings (Key, Value, LastUpdated)
VALUES (@key, @value, @timestamp)", connection);
command.Parameters.AddWithValue("@key", SettingsKey);
command.Parameters.AddWithValue("@value", json);
command.Parameters.AddWithValue("@timestamp", timestamp);

await command.ExecuteNonQueryAsync();
}
}
12 changes: 12 additions & 0 deletions src/StatusLightChecker.Core/Services/Interfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ public interface IColorConfigurationService
event EventHandler<ColorConfigurationChangedEventArgs>? ConfigurationChanged;
}

public record SerialPortConfiguration
{
public string ComPort { get; init; } = "COM3";
public int BaudRate { get; init; } = 115200;
}

public interface ISerialPortConfigurationService
{
SerialPortConfiguration GetCurrentConfiguration();
Task UpdateConfigurationAsync(SerialPortConfiguration configuration);
}

public interface IStatusDetector
{
string ApplicationName { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@
<ItemGroup Condition="!Exists('$(ClientPublishDir)') or !Exists('$(ServicePublishDir)')">
<ProjectReference Include="..\StatusLightChecker\StatusLightChecker.csproj">
<Targets>Publish</Targets>
<Properties>Configuration=$(Configuration);RuntimeIdentifier=win-x64;SelfContained=true;TargetFramework=net10.0-windows;PublishDir=$(ClientPublishDir)</Properties>
<Properties>Configuration=$(Configuration);RuntimeIdentifier=win-x64;SelfContained=false;TargetFramework=net10.0-windows;PublishDir=$(ClientPublishDir)</Properties>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
</ProjectReference>
<ProjectReference Include="..\StatusLightChecker.Service\StatusLightChecker.Service.csproj">
<Targets>Publish</Targets>
<Properties>Configuration=$(Configuration);RuntimeIdentifier=win-x64;SelfContained=true;TargetFramework=net10.0-windows;PublishDir=$(ServicePublishDir)</Properties>
<Properties>Configuration=$(Configuration);RuntimeIdentifier=win-x64;SelfContained=false;TargetFramework=net10.0-windows;PublishDir=$(ServicePublishDir)</Properties>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
</ProjectReference>
Expand Down
71 changes: 71 additions & 0 deletions src/StatusLightChecker.Installer/StatusLightCheckerSetup.iss
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,77 @@ begin
Result := ExpandConstant('{userappdata}\Jublin\StatusLightChecker');
end;

{ Check registry for any installed .NET Desktop Runtime 10.x (64-bit). }
function IsDotNetDesktopRuntimeInstalled: Boolean;
const
RegKey = 'SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App';
var
Names: TArrayOfString;
I: Integer;
begin
Result := False;
if RegGetValueNames(HKLM64, RegKey, Names) then
for I := 0 to High(Names) do
if Copy(Names[I], 1, 3) = '10.' then
begin
Result := True;
Break;
end;
end;

{ Download .NET Desktop Runtime 10 via PowerShell and install silently. }
function DownloadAndInstallDotNet: Boolean;
var
InstallerPath, PSArgs: String;
RC: Integer;
begin
Result := False;
InstallerPath := ExpandConstant('{tmp}\windowsdesktop-runtime-10-x64.exe');

PSArgs := '-NoProfile -NonInteractive -Command ' +
'"Invoke-WebRequest -Uri ''https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x64.exe''' +
' -OutFile ''' + InstallerPath + '''"';

if not Exec('powershell.exe', PSArgs, '', SW_HIDE, ewWaitUntilTerminated, RC) or (RC <> 0) then
begin
MsgBox(
'Failed to download .NET Desktop Runtime 10.' + #13#10 +
'Please install it manually from https://dotnet.microsoft.com/download/dotnet/10.0' + #13#10 +
'then re-run this installer.',
mbError, MB_OK);
Exit;
end;

Exec(InstallerPath, '/install /quiet /norestart', '', SW_SHOW, ewWaitUntilTerminated, RC);
{ RC=0: success; RC=3010: success, reboot required — both are fine. }
Result := (RC = 0) or (RC = 3010);
if not Result then
MsgBox(
'.NET Desktop Runtime 10 installer exited with code ' + IntToStr(RC) + '.' + #13#10 +
'Please install it manually and re-run this installer.',
mbError, MB_OK);
end;

{ Abort setup if .NET Desktop Runtime 10 is absent and user declines to install it. }
function InitializeSetup: Boolean;
begin
Result := True;
if IsDotNetDesktopRuntimeInstalled then
Exit;

if MsgBox(
'.NET Desktop Runtime 10.0 is required but was not found on this machine.' + #13#10 + #13#10 +
'Click OK to download and install it now (~55 MB),' + #13#10 +
'or Cancel to exit the installer.',
mbConfirmation, MB_OKCANCEL) <> IDOK then
begin
Result := False;
Exit;
end;

Result := DownloadAndInstallDotNet;
end;

{ Warn the user if they chose non-admin install: service won't run. }
procedure CurStepChanged(CurStep: TSetupStep);
begin
Expand Down
8 changes: 8 additions & 0 deletions src/StatusLightChecker.Service/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using StatusLightChecker.Core.Services;
using StatusLightChecker.Service.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -57,6 +58,13 @@
return new ColorConfigurationService(logger, dbConnectionString);
});

builder.Services.AddSingleton<ISerialPortConfigurationService>(sp =>
{
var logger = sp.GetRequiredService<ILogger<SerialPortConfigurationService>>();
var config = sp.GetRequiredService<IConfiguration>();
return new SerialPortConfigurationService(logger, dbConnectionString, config);
});

builder.Services.AddSingleton<ServiceHealthTracker>();
builder.Services.AddSingleton<IServiceHealthTracker>(sp => sp.GetRequiredService<ServiceHealthTracker>());

Expand Down
Loading
Loading