Project initial upload

This commit is contained in:
NinjaPug
2025-03-28 11:31:30 -04:00
parent 9d80c6a125
commit 553849d67f
24 changed files with 6585 additions and 2 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/PCPalConfigurator/bin
/PCPalConfigurator/obj
/PCPalService/.vs
/PCPalConfigurator/.vs
/PCPalService/bin
/PCPalService/obj

74
ManageService.ps1 Normal file
View File

@@ -0,0 +1,74 @@
# Define the service name and path to the published service
$ServiceName = "ESP32BackgroundService"
$ServiceExePath = "$PSScriptRoot\ESP32BackgroundService.exe"
# Function to check if the service exists
function ServiceExists {
return Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
}
# Install the service
function Install-Service {
if (ServiceExists) {
Write-Host "Service '$ServiceName' already exists."
return
}
Write-Host "Installing service '$ServiceName'..."
sc.exe create $ServiceName binPath= "`"$ServiceExePath`"" start= auto
sc.exe failure $ServiceName reset= 0 actions= restart/5000
Start-Service -Name $ServiceName
Write-Host "Service installed and started successfully with auto-restart enabled."
}
# Start the service
function Start-Service {
if (ServiceExists) {
Write-Host "Starting service '$ServiceName'..."
sc.exe start $ServiceName
Write-Host "Service started successfully."
} else {
Write-Host "Service '$ServiceName' is not installed."
}
}
# Stop the service
function Stop-Service {
if (ServiceExists) {
Write-Host "Stopping service '$ServiceName'..."
sc.exe stop $ServiceName
Write-Host "Service stopped successfully."
} else {
Write-Host "Service '$ServiceName' is not installed."
}
}
# Uninstall the service
function Uninstall-Service {
if (ServiceExists) {
Stop-Service
Write-Host "Uninstalling service '$ServiceName'..."
sc.exe delete $ServiceName
Write-Host "Service uninstalled successfully."
} else {
Write-Host "Service '$ServiceName' is not installed."
}
}
# Menu for user selection
Write-Host "Choose an option:"
Write-Host "1) Install Service"
Write-Host "2) Start Service"
Write-Host "3) Stop Service"
Write-Host "4) Uninstall Service"
Write-Host "5) Exit"
$choice = Read-Host "Enter your choice (1-5)"
switch ($choice) {
"1" { Install-Service }
"2" { Start-Service }
"3" { Stop-Service }
"4" { Uninstall-Service }
"5" { Write-Host "Exiting script..." }
default { Write-Host "Invalid choice. Please select a valid option." }
}

View File

@@ -0,0 +1,60 @@
#include <Wire.h>
#include <LiquidCrystal_PCF8574.h>
#define I2C_ADDR 0x27 // I2C address of the LCD module
#define SDA_PIN 8 // ESP32-C3 SuperMini SDA
#define SCL_PIN 9 // ESP32-C3 SuperMini SCL
LiquidCrystal_PCF8574 lcd(I2C_ADDR);
void setup() {
Serial.begin(115200); // USB/UART Serial for PC communication
Wire.begin(SDA_PIN, SCL_PIN); // Set I2C pins
// Initialize LCD
lcd.begin(16, 2);
lcd.setBacklight(255);
lcd.clear();
lcd.setCursor(2, 0);
lcd.print("ThermalTake");
lcd.setCursor(3, 1);
lcd.print("Tower 300");
Serial.println("ESP32-C3 SuperMini Ready");
}
void loop() {
if (Serial.available()) {
String command = Serial.readStringUntil('\n'); // Read input from PC
command.trim();
if (command.startsWith("CMD:LCD,")) {
handleLCDCommand(command);
} else if (command == "CMD:GET_LCD_TYPE") {
Serial.println("LCD_TYPE:1602A");
}
}
}
// Parse and execute LCD update command
void handleLCDCommand(String command) {
command.replace("CMD:LCD,", ""); // Remove the command header
int commaIndex = command.indexOf(',');
if (commaIndex == -1) return; // Invalid format
String lineNumber = command.substring(0, commaIndex);
String text = command.substring(commaIndex + 1);
int line = lineNumber.toInt();
if (line >= 0 && line < 2) { // Only two lines (0 and 1)
lcd.setCursor(0, line);
lcd.print(" "); // Clear line
lcd.setCursor(0, line);
lcd.print(text);
Serial.println("LCD Updated");
} else {
Serial.println("ERROR: Invalid LCD Line");
}
}

View File

@@ -0,0 +1,15 @@
namespace PCPalConfigurator
{
public class ConfigData
{
public string LastUsedPort { get; set; }
public string ScreenType { get; set; }
public string Line1Selection { get; set; }
public string Line1CustomText { get; set; }
public string Line2Selection { get; set; }
public string Line2CustomText { get; set; }
public string Line1PostText { get; set; }
public string Line2PostText { get; set; }
}
}

View File

@@ -0,0 +1,148 @@
namespace PCPalConfigurator
{
partial class ConfiguratorForm
{
private System.ComponentModel.IContainer components = null;
private TabControl tabControl;
private TabPage tab1602;
private TabPage tabTFT;
private TabPage tabAbout;
private ComboBox cmbLine1;
private TextBox txtLine1;
private TextBox txtLine1Post;
private ComboBox cmbLine2;
private TextBox txtLine2;
private TextBox txtLine2Post;
private Button btnApply;
private Label lblLine1;
private Label lblLine1Prefix;
private Label lblLine1Suffix;
private Label lblLine2;
private Label lblLine2Prefix;
private Label lblLine2Suffix;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
components.Dispose();
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.tabControl = new TabControl();
this.tab1602 = new TabPage("1602 LCD");
this.tabTFT = new TabPage("4.6\" TFT LCD");
this.tabAbout = new TabPage("About");
this.cmbLine1 = new ComboBox();
this.txtLine1 = new TextBox();
this.txtLine1Post = new TextBox();
this.cmbLine2 = new ComboBox();
this.txtLine2 = new TextBox();
this.txtLine2Post = new TextBox();
this.btnApply = new Button();
this.lblLine1 = new Label();
this.lblLine1Prefix = new Label();
this.lblLine1Suffix = new Label();
this.lblLine2 = new Label();
this.lblLine2Prefix = new Label();
this.lblLine2Suffix = new Label();
// === TabControl ===
this.tabControl.Location = new System.Drawing.Point(10, 10);
this.tabControl.Size = new System.Drawing.Size(620, 280);
this.tabControl.TabPages.Add(this.tab1602);
this.tabControl.TabPages.Add(this.tabTFT);
this.tabControl.TabPages.Add(this.tabAbout);
this.Controls.Add(this.tabControl);
// === Line 1 UI ===
this.lblLine1.Text = "Line 1:";
this.lblLine1.Location = new System.Drawing.Point(20, 20);
this.lblLine1.AutoSize = true;
this.cmbLine1.Location = new System.Drawing.Point(100, 18);
this.cmbLine1.Size = new System.Drawing.Size(200, 21);
this.lblLine1Prefix.Text = "Prefix:";
this.lblLine1Prefix.Location = new System.Drawing.Point(20, 50);
this.lblLine1Prefix.AutoSize = true;
this.txtLine1.Location = new System.Drawing.Point(100, 48);
this.txtLine1.Size = new System.Drawing.Size(200, 21);
this.lblLine1Suffix.Text = "Suffix / Units:";
this.lblLine1Suffix.Location = new System.Drawing.Point(320, 50);
this.lblLine1Suffix.AutoSize = true;
this.txtLine1Post.Location = new System.Drawing.Point(420, 48);
this.txtLine1Post.Size = new System.Drawing.Size(120, 21);
// === Line 2 UI ===
this.lblLine2.Text = "Line 2:";
this.lblLine2.Location = new System.Drawing.Point(20, 90);
this.lblLine2.AutoSize = true;
this.cmbLine2.Location = new System.Drawing.Point(100, 88);
this.cmbLine2.Size = new System.Drawing.Size(200, 21);
this.lblLine2Prefix.Text = "Prefix:";
this.lblLine2Prefix.Location = new System.Drawing.Point(20, 120);
this.lblLine2Prefix.AutoSize = true;
this.txtLine2.Location = new System.Drawing.Point(100, 118);
this.txtLine2.Size = new System.Drawing.Size(200, 21);
this.lblLine2Suffix.Text = "Suffix / Units:";
this.lblLine2Suffix.Location = new System.Drawing.Point(320, 120);
this.lblLine2Suffix.AutoSize = true;
this.txtLine2Post.Location = new System.Drawing.Point(420, 118);
this.txtLine2Post.Size = new System.Drawing.Size(120, 21);
// === Apply Button ===
this.btnApply.Text = "Apply";
this.btnApply.Location = new System.Drawing.Point(20, 170);
this.btnApply.Size = new System.Drawing.Size(100, 30);
this.btnApply.Click += new System.EventHandler(this.btnApply_Click);
// === Add to 1602 Tab ===
this.tab1602.Controls.AddRange(new Control[]
{
lblLine1, cmbLine1, lblLine1Prefix, txtLine1, lblLine1Suffix, txtLine1Post,
lblLine2, cmbLine2, lblLine2Prefix, txtLine2, lblLine2Suffix, txtLine2Post,
btnApply
});
// === TFT Tab Placeholder ===
Label lblTFT = new Label()
{
Text = "TFT configuration coming soon...",
Location = new System.Drawing.Point(20, 20),
AutoSize = true
};
this.tabTFT.Controls.Add(lblTFT);
// === About Tab ===
Label lblAbout = new Label()
{
Text = "PCPal Display Configurator\nFor ThermalTake Tower XXX\nVersion 1.0.0\n© 2025 Christopher Koch aka NinjaPug",
Location = new System.Drawing.Point(20, 20),
AutoSize = true
};
this.tabAbout.Controls.Add(lblAbout);
// === Form ===
this.Text = "Display Configurator";
this.ClientSize = new System.Drawing.Size(640, 300);
this.ResumeLayout(false);
this.PerformLayout();
}
}
}

View File

@@ -0,0 +1,120 @@
using System;
using System.IO;
using System.Windows.Forms;
using Newtonsoft.Json;
using LibreHardwareMonitor.Hardware;
using PCPalConfigurator;
namespace PCPalConfigurator
{
public partial class ConfiguratorForm : Form
{
private ConfigData config;
private readonly string ConfigFile = GetConfigPath();
private Computer computer;
public ConfiguratorForm()
{
InitializeComponent();
InitHardware();
LoadConfig();
}
private void InitHardware()
{
computer = new Computer
{
IsCpuEnabled = true,
IsGpuEnabled = true,
IsMemoryEnabled = true,
IsMotherboardEnabled = true,
IsControllerEnabled = true,
IsStorageEnabled = true
};
computer.Open();
PopulateSensorOptions();
}
private static string GetConfigPath()
{
string folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ESP32Display");
Directory.CreateDirectory(folder);
return Path.Combine(folder, "config.json");
}
private void LoadConfig()
{
if (File.Exists(ConfigFile))
{
string json = File.ReadAllText(ConfigFile);
config = JsonConvert.DeserializeObject<ConfigData>(json) ?? new ConfigData();
ApplyConfig();
}
else
{
config = new ConfigData();
}
}
private void ApplyConfig()
{
cmbLine1.SelectedItem = config.Line1Selection;
txtLine1.Text = config.Line1CustomText;
txtLine1Post.Text = config.Line1PostText;
cmbLine2.SelectedItem = config.Line2Selection;
txtLine2.Text = config.Line2CustomText;
txtLine2Post.Text = config.Line2PostText;
}
private void SaveConfig()
{
// ScreenType based on selected tab
if (tabControl.SelectedTab == tab1602)
config.ScreenType = "1602";
else if (tabControl.SelectedTab == tabTFT)
config.ScreenType = "TFT4_6";
config.Line1Selection = cmbLine1.SelectedItem?.ToString();
config.Line1CustomText = txtLine1.Text;
config.Line1PostText = txtLine1Post.Text;
config.Line2Selection = cmbLine2.SelectedItem?.ToString();
config.Line2CustomText = txtLine2.Text;
config.Line2PostText = txtLine2Post.Text;
string json = JsonConvert.SerializeObject(config, Formatting.Indented);
File.WriteAllText(ConfigFile, json);
MessageBox.Show("Settings saved!");
}
private void btnApply_Click(object sender, EventArgs e)
{
SaveConfig();
}
private void PopulateSensorOptions()
{
cmbLine1.Items.Clear();
cmbLine2.Items.Clear();
foreach (var hardware in computer.Hardware)
{
hardware.Update();
foreach (var sensor in hardware.Sensors)
{
if (sensor.SensorType == SensorType.Load ||
sensor.SensorType == SensorType.Temperature ||
sensor.SensorType == SensorType.Data ||
sensor.SensorType == SensorType.Fan)
{
string option = $"{hardware.HardwareType}: {sensor.Name} ({sensor.SensorType})";
cmbLine1.Items.Add(option);
cmbLine2.Items.Add(option);
}
}
}
cmbLine1.Items.Add("Custom Text");
cmbLine2.Items.Add("Custom Text");
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Compile Update="ConfiguratorForm.cs">
<SubType>Form</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35828.75 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCPalConfigurator", "PCPalConfigurator.csproj", "{DC3430AF-194C-4FA9-A675-DE0C22E3A160}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DC3430AF-194C-4FA9-A675-DE0C22E3A160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC3430AF-194C-4FA9-A675-DE0C22E3A160}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC3430AF-194C-4FA9-A675-DE0C22E3A160}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC3430AF-194C-4FA9-A675-DE0C22E3A160}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B16D071E-66FA-43E7-AF92-3D54286834FF}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,14 @@
namespace PCPalConfigurator
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ConfiguratorForm());
}
}
}

View File

@@ -0,0 +1,8 @@
{
"LastUsedPort": null,
"ScreenType": null,
"Line1Selection": "Cpu: CPU Total (Load)",
"Line1CustomText": "",
"Line2Selection": "Memory: Memory (Load)",
"Line2CustomText": "rfrfrf"
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-PCPalService-11cebed9-d814-495b-a86d-2d912052e92d</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IO.Ports" Version="9.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35828.75 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCPalService", "PCPalService.csproj", "{71DA320E-9016-4178-89D7-32DEF26AE4E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{71DA320E-9016-4178-89D7-32DEF26AE4E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71DA320E-9016-4178-89D7-32DEF26AE4E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71DA320E-9016-4178-89D7-32DEF26AE4E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71DA320E-9016-4178-89D7-32DEF26AE4E1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {50DABD9C-9653-4E3A-8800-8A3D4C5CB3BF}
EndGlobalSection
EndGlobal

22
PCPalService/Program.cs Normal file
View File

@@ -0,0 +1,22 @@
using PCPalService;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
namespace PCPalService
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"PCPalService": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

192
PCPalService/Worker.cs Normal file
View File

@@ -0,0 +1,192 @@
using System;
using System.IO;
using System.IO.Ports;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using LibreHardwareMonitor.Hardware;
using Newtonsoft.Json;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace PCPalService
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private SerialPort serialPort;
private Computer computer;
private readonly string ConfigFile = GetConfigPath();
private const string LogFile = "service_log.txt";
public Worker(ILogger<Worker> logger)
{
_logger = logger;
computer = new Computer { IsCpuEnabled = true, IsMemoryEnabled = true };
computer.Open();
}
private static string GetConfigPath()
{
string folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PCPal");
Directory.CreateDirectory(folder); // Ensure folder exists
return Path.Combine(folder, "config.json");
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Log("ESP32 Background Service Starting...");
try
{
string portName = AutoDetectESP32();
if (portName == null)
{
Log("ESP32-C3 not found. Service stopping...");
throw new Exception("ESP32 not detected.");
}
serialPort = new SerialPort(portName, 115200);
serialPort.Open();
Log($"Connected to ESP32-C3 on {portName}");
while (!stoppingToken.IsCancellationRequested)
{
UpdateLCD();
await Task.Delay(5000, stoppingToken);
}
}
catch (Exception ex)
{
Log($"Fatal Error: {ex.Message}");
Environment.Exit(1); // Force exit, triggering restart
}
}
private string AutoDetectESP32()
{
foreach (string port in SerialPort.GetPortNames())
{
if (IsESP32Device(port)) return port;
}
return null;
}
private bool IsESP32Device(string portName)
{
try
{
using (SerialPort testPort = new SerialPort(portName, 115200))
{
testPort.Open();
testPort.WriteLine("CMD:GET_LCD_TYPE");
Thread.Sleep(500);
string response = testPort.ReadExisting();
testPort.Close();
return response.Contains("LCD_TYPE:1602A");
}
}
catch { return false; }
}
private void UpdateLCD()
{
ConfigData config = LoadConfig();
string line1 = GetLCDContent(config.Line1Selection, config.Line1CustomText, config.Line1PostText);
string line2 = GetLCDContent(config.Line2Selection, config.Line2CustomText, config.Line2PostText);
SendCommand($"CMD:LCD,0,{line1}");
SendCommand($"CMD:LCD,1,{line2}");
}
private string GetLCDContent(string selection, string prefix, string postfix)
{
var parsed = ParseSensorSelection(selection);
if (parsed == null)
return "N/A";
string value = GetSensorValue(parsed.Value.hardwareType, parsed.Value.sensorName, parsed.Value.sensorType);
return $"{prefix}{value}{postfix}";
}
private (HardwareType hardwareType, string sensorName, SensorType sensorType)? ParseSensorSelection(string input)
{
try
{
var parts = input.Split(':', 2);
if (parts.Length != 2) return null;
var hardwareType = Enum.Parse<HardwareType>(parts[0].Trim());
var sensorInfo = parts[1].Trim();
int idx = sensorInfo.LastIndexOf('(');
if (idx == -1) return null;
string name = sensorInfo[..idx].Trim();
string typeStr = sensorInfo[(idx + 1)..].Trim(' ', ')');
var sensorType = Enum.Parse<SensorType>(typeStr);
return (hardwareType, name, sensorType);
}
catch
{
return null;
}
}
private string GetSensorValue(HardwareType type, string name, SensorType sensorType)
{
foreach (IHardware hardware in computer.Hardware)
{
if (hardware.HardwareType == type)
{
hardware.Update();
foreach (ISensor sensor in hardware.Sensors)
{
if (sensor.SensorType == sensorType && sensor.Name == name)
{
return sensor.Value?.ToString("0.0") ?? "N/A";
}
}
}
}
return "N/A";
}
private ConfigData LoadConfig()
{
if (File.Exists(ConfigFile))
{
string json = File.ReadAllText(ConfigFile);
return JsonConvert.DeserializeObject<ConfigData>(json) ?? new ConfigData();
}
return new ConfigData();
}
private void SendCommand(string command)
{
serialPort.WriteLine(command);
Log($"Sent: {command}");
}
private void Log(string message)
{
string logEntry = $"{DateTime.Now}: {message}";
File.AppendAllText(LogFile, logEntry + Environment.NewLine);
_logger.LogInformation(message);
}
}
class ConfigData
{
public string Line1Selection { get; set; }
public string Line1CustomText { get; set; }
public string Line2Selection { get; set; }
public string Line2CustomText { get; set; }
public string Line1PostText { get; set; }
public string Line2PostText { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

10
PCPalService/config.json Normal file
View File

@@ -0,0 +1,10 @@
{
"LastUsedPort": null,
"ScreenType": null,
"Line1Selection": "Cpu: CPU Total (Load)",
"Line1CustomText": "CPU ",
"Line2Selection": "Memory: Memory Used (Data)",
"Line2CustomText": "Memory ",
"Line1PostText": "%",
"Line2PostText": "GB"
}

5543
PCPalService/service_log.txt Normal file

File diff suppressed because it is too large Load Diff

137
README.md
View File

@@ -1,2 +1,135 @@
# PCPal
ThermalTake Tower X00 LCD Alternative
# LCD Display Configurator
A Windows-based configuration utility and background service for customizing text output on ESP32-connected displays such as a 1602 LCD or a 4.6" TFT screen.
![Banner Image Placeholder](docs/images/banner.jpg)
---
## 📦 Features
- 🔌 Communicates with ESP32 over USB serial
- 📊 Displays real-time system metrics (CPU, RAM, etc.)
- ⚙️ Full GUI configurator with support for:
- 1602 LCD
- 4.6" TFT LCD (placeholder for future)
- 📁 Config stored in AppData (shared between GUI and background service)
- 🔄 Windows service auto-starts and auto-recovers on crash
- 💡 Modular and future-proof design for multiple display types
---
## 🖥️ Configuration UI
The WinForms-based configuration tool allows you to:
- Select system metrics using a dropdown populated from LibreHardwareMonitor
- Customize line prefixes (e.g., `CPU: `) and suffixes (e.g., `%`, `°C`)
- Configure different screen types via tabbed interface
### 🔧 1602 LCD Tab
- Line-by-line customization
- Metric + prefix/suffix control
- Saves to shared config file in AppData
> ![1602 LCD Tab Screenshot Placeholder](docs/images/1602.png)
---
### 📺 4.6" TFT LCD Tab
- Coming soon! Layout planned for graphical display customization
---
### About Tab
Displays app info, version, and author attribution.
---
## 🛠️ Windows Background Service
- Continuously reads system metrics
- Sends output to ESP32-connected LCD
- Detects connected COM port automatically
- Reloads config when `config.json` changes
- Auto-recovers if the service crashes
### Service Config Example:
```json
{
"ScreenType": "1602",
"Line1Selection": "Cpu: CPU Total (Load)",
"Line1CustomText": "CPU: ",
"Line1PostText": "%",
"Line2Selection": "Memory: Used Memory (Data)",
"Line2CustomText": "RAM: ",
"Line2PostText": " GB"
}
```
---
## 📂 Config File Location
The config file is shared between the UI and service and stored here:
```
%AppData%\PCPal\config.json
```
---
## 📦 Installation
1. **Clone the repo**
2. Build both projects:
- `DisplayConfigurator` (WinForms app)
- `ESP32BackgroundService` (Worker service)
3. Publish the service and install via `sc.exe` or PowerShell
4. Run the configurator to apply your settings
---
## 🧪 Development
- Built using `.NET 6+`
- Uses:
- [`LibreHardwareMonitorLib`](https://github.com/LibreHardwareMonitor/LibreHardwareMonitor)
- `Newtonsoft.Json`
- `System.IO.Ports`
---
## 🧰 Tools & Commands
### Install the Service:
```powershell
sc create ESP32BackgroundService binPath= "C:\Path\To\ESP32BackgroundService.exe"
sc failure ESP32BackgroundService reset= 0 actions= restart/5000
```
### Uninstall the Service:
```powershell
sc stop ESP32BackgroundService
sc delete ESP32BackgroundService
```
---
## 📸 Screenshots
> Replace the image paths with real screenshots when available.
- `docs/images/banner.jpg` project header
- `docs/images/1602.png` 1602 LCD config tab
---
## 📄 License
MIT License
© 2025 Christopher Koch aka, NinjaPug

BIN
docs/images/1602.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/images/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB