initial version with serial link

This commit is contained in:
2026-01-18 02:25:38 +01:00
parent d432db9985
commit 2311647885
11 changed files with 582 additions and 463 deletions

View File

@@ -0,0 +1,54 @@
COMMAND BEHAVIOR SUMMARY
========================
PING
----
Format:
PING
Response:
Analog_System_Monitor_<version>
Purpose:
- Confirms the device is alive
- Allows PC-side auto-discovery
- Returns firmware version
SETALL
------
Format:
SETALL: v0,v1,v2,v3,v4,v5,v6,v7
Rules:
- Exactly 8 comma-separated values
- Each value must be numeric
- Each value must be between 0 and 100
- No extra values allowed
- No missing values allowed
- No trailing commas or extra characters
Success Response:
OK
Error Response:
ERROR
Purpose:
- Updates all 8 channels in one atomic operation
- Applies calibration to each channel
- Resets watchdog timer
UNKNOWN / MALFORMED COMMANDS
----------------------------
Format:
Anything not matching PING or a valid SETALL command
Response:
ERROR
Purpose:
- Keeps protocol strict and predictable
- Prevents accidental meter movement
- Simplifies debugging

View File

@@ -103,19 +103,68 @@ void loop() {
continue; continue;
} }
int eq = s.indexOf('='); // --- Batch update command ---
if (eq > 0) { if (s.startsWith("SETALL:")) {
int ch = s.substring(0, eq).toInt(); String payload = s.substring(7);
float val = s.substring(eq + 1).toFloat(); payload.trim();
if (ch >= 0 && ch < NUM_CHANNELS && val >= 0.0f && val <= 100.0f) {
currentDuty[ch] = val; float newValues[NUM_CHANNELS];
int count = 0;
bool error = false;
// Parse up to NUM_CHANNELS values
while (payload.length() > 0 && count < NUM_CHANNELS) {
int comma = payload.indexOf(',');
String part;
if (comma >= 0) {
part = payload.substring(0, comma);
payload = payload.substring(comma + 1);
} else {
part = payload;
payload = "";
}
part.trim();
if (part.length() == 0) {
error = true;
break;
}
float val = part.toFloat();
if (val < 0.0f || val > 100.0f) {
error = true;
break;
}
newValues[count++] = val;
}
// Conditions for a valid SETALL:
// - exactly NUM_CHANNELS values parsed
// - no leftover payload
// - no parse/range errors
if (error || count != NUM_CHANNELS || payload.length() > 0) {
Serial.println("ERROR");
continue;
}
// All good → apply values
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
currentDuty[ch] = newValues[ch];
float calibratedDuty = applyCalibration(ch, currentDuty[ch]); float calibratedDuty = applyCalibration(ch, currentDuty[ch]);
pwm[ch]->setPWM(pwmPins[ch], pwmFrequency, calibratedDuty); pwm[ch]->setPWM(pwmPins[ch], pwmFrequency, calibratedDuty);
lastSerialTime = millis(); // reset watchdog
} }
Serial.println("OK");
lastSerialTime = millis();
continue;
}
// --- Unknown command ---
if (s.length() > 0) {
Serial.println("ERROR");
} }
} }

View File

@@ -1,44 +0,0 @@
using System;
using System.IO;
using System.Text.Json;
public class Config
{
public string OscIp { get; set; } = "127.0.0.1";
public int OscPort { get; set; } = 9000;
public int UpdateRateMs { get; set; } = 1000;
private static string ConfigPath =>
Path.Combine(AppContext.BaseDirectory, "config.json");
public static Config Load()
{
try
{
if (File.Exists(ConfigPath))
{
string json = File.ReadAllText(ConfigPath);
return JsonSerializer.Deserialize<Config>(json) ?? new Config();
}
}
catch { }
// If loading fails, create a default config
var cfg = new Config();
cfg.Save();
return cfg;
}
public void Save()
{
try
{
string json = JsonSerializer.Serialize(this, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(ConfigPath, json);
}
catch { }
}
}

View File

@@ -1,15 +1,12 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
namespace analog_system_monitor internal static class Program
{ {
internal static class Program [STAThread]
static void Main()
{ {
[STAThread] ApplicationConfiguration.Initialize();
static void Main() Application.Run(new TrayApp());
{
ApplicationConfiguration.Initialize();
Application.Run(new TrayApp());
}
} }
} }

View File

@@ -0,0 +1,158 @@
#nullable enable
using System;
using System.IO.Ports;
using System.Linq;
using System.Threading;
public class SerialManager : IDisposable
{
private SerialPort? port;
private DateTime lastResponse = DateTime.UtcNow;
private const int BaudRate = 115200;
private const int PingTimeoutMs = 300;
private const int WatchdogTimeoutMs = 3000;
public bool IsConnected => port != null && port.IsOpen;
// ---------------- DEVICE DISCOVERY ----------------
public void DiscoverDevice()
{
DisposePort();
foreach (string com in SerialPort.GetPortNames().OrderBy(s => s))
{
string? response = null;
try
{
using (var testPort = CreatePort(com, PingTimeoutMs))
{
testPort.Open();
Thread.Sleep(350); // RP2040 resets on open
testPort.DiscardInBuffer();
testPort.DiscardOutBuffer();
testPort.Write("PING\n");
try
{
if (testPort.BytesToRead > 0)
response = testPort.ReadLine();
}
catch (TimeoutException)
{
// No response — move to next port
}
}
if (response != null &&
response.StartsWith("Analog_System_Monitor_"))
{
port = CreatePort(com, 500);
port.Open();
Thread.Sleep(350); // allow RP2040 reboot again
lastResponse = DateTime.UtcNow;
return;
}
}
catch
{
// Silent fail — move to next COM port
}
}
port = null;
}
// ---------------- PORT FACTORY ----------------
private SerialPort CreatePort(string com, int timeout)
{
return new SerialPort(com, BaudRate)
{
Parity = Parity.None,
DataBits = 8,
StopBits = StopBits.One,
Handshake = Handshake.None,
NewLine = "\n",
Encoding = System.Text.Encoding.ASCII,
ReadTimeout = timeout,
WriteTimeout = timeout,
DtrEnable = true, // REQUIRED for RP2040 USB CDC
RtsEnable = true
};
}
// ---------------- WATCHDOG ----------------
public bool WatchdogExpired =>
(DateTime.UtcNow - lastResponse).TotalMilliseconds > WatchdogTimeoutMs;
// ---------------- SEND COMMAND ----------------
public void SendSetAll(string cmd)
{
if (!IsConnected)
return;
try
{
port!.Write(cmd + "\n");
string? response = null;
try
{
if (port.BytesToRead > 0)
response = port.ReadLine();
}
catch (TimeoutException)
{
// No response this tick — totally fine
}
if (response != null &&
(response.StartsWith("OK") || response.StartsWith("ERROR")))
{
lastResponse = DateTime.UtcNow;
}
}
catch
{
// Silent fail — watchdog will trigger reconnect
}
}
// ---------------- CLEANUP ----------------
private void DisposePort()
{
try
{
if (port != null)
{
port.Close();
port.Dispose();
}
}
catch
{
// ignore
}
port = null;
}
public void Dispose()
{
DisposePort();
}
}

View File

@@ -1,338 +1,242 @@
#nullable enable
using System; using System;
using System.Net.Sockets; using System.Globalization;
using System.Text;
using System.Collections.Generic;
using LibreHardwareMonitor.Hardware; using LibreHardwareMonitor.Hardware;
public class Telemetry public class Telemetry : IDisposable
{ {
private Config config; private const int UpdateRateDefaultMs = 1000;
private string oscIp = "127.0.0.1"; public int UpdateRateMs => UpdateRateDefaultMs;
private int oscPort = 9000;
private readonly SerialManager serial = new SerialManager();
private readonly Computer computer = new Computer();
// Cached hardware references
private IHardware? cpuHw;
private IHardware? gpuHw;
private IHardware? memHw;
// Cached sensors
private ISensor[] cpuLoadSensors = Array.Empty<ISensor>(); private ISensor[] cpuLoadSensors = Array.Empty<ISensor>();
private ISensor? cpuTempSensor; private ISensor? cpuTempSensor;
private ISensor? gpuVramUsedSensor;
private ISensor? gpuVramTotalSensor;
private ISensor? gpu3DLoadSensor; private ISensor? gpu3DLoadSensor;
private ISensor? gpuTempSensor; private ISensor? gpuTempSensor;
private ISensor? gpuVramUsedSensor;
private ISensor? gpuVramTotalSensor;
private Computer computer = new Computer(); private ISensor? memUsedSensor;
private UdpClient udp = new UdpClient(); private ISensor? memAvailSensor;
// ---------------- INITIALIZATION ---------------- private static readonly CultureInfo CI = CultureInfo.InvariantCulture;
public void Initialize() public void Initialize()
{
config = Config.Load();
// Load defaults from config
oscIp = config.OscIp;
oscPort = config.OscPort;
// Override with command-line args if provided
ParseArgs(Environment.GetCommandLineArgs());
// Save updated config (optional)
config.OscIp = oscIp;
config.OscPort = oscPort;
config.Save();
computer.IsMemoryEnabled = true;
computer.IsCpuEnabled = true;
computer.IsGpuEnabled = true;
computer.Open();
DetectSensors();
}
public int UpdateRateMs => config.UpdateRateMs;
private void ParseArgs(string[] args)
{
foreach (var arg in args)
{ {
if (arg.StartsWith("--ip=")) serial.DiscoverDevice();
oscIp = arg.Substring("--ip=".Length);
if (arg.StartsWith("--port=") && // Enable only what we need
int.TryParse(arg.Substring("--port=".Length), out int p)) computer.IsCpuEnabled = true;
oscPort = p; computer.IsGpuEnabled = true;
computer.IsMemoryEnabled = true;
if (arg.StartsWith("--rate=") && computer.IsMotherboardEnabled = false;
int.TryParse(arg.Substring("--rate=".Length), out int r)) computer.IsControllerEnabled = false;
config.UpdateRateMs = r; computer.IsNetworkEnabled = false;
} computer.IsStorageEnabled = false;
} computer.IsBatteryEnabled = false;
computer.IsPsuEnabled = false;
computer.Open();
// ---------------- MAIN UPDATE LOOP ---------------- CacheHardwareAndSensors();
public void UpdateAndSend()
{
int memPercent = GetMemoryUsagePercent();
int cpuPercent = GetCpuLoadPercent();
int vramPercent = GetGpuVramPercent();
int gpu3DPercent = GetGpu3DLoad();
int cpuTempPercent = GetCpuTemperaturePercent();
int gpuTempPercent = GetGpuTemperaturePercent();
SendOscBundle(
cpuPercent / 100f,
cpuTempPercent / 100f,
memPercent / 100f,
gpu3DPercent / 100f,
gpuTempPercent / 100f,
vramPercent / 100f
);
} }
// ---------------- SENSOR DETECTION ---------------- private void CacheHardwareAndSensors()
private void DetectSensors()
{ {
var cpuLoadList = new List<ISensor>();
ISensor? bestCpuTemp = null;
ISensor? bestGpuTemp = null;
ISensor? bestGpu3D = null;
ISensor? bestVramUsed = null;
ISensor? bestVramTotal = null;
foreach (var hw in computer.Hardware) foreach (var hw in computer.Hardware)
{ {
hw.Update(); hw.Update();
if (hw.HardwareType == HardwareType.Cpu) switch (hw.HardwareType)
{ {
foreach (var sensor in hw.Sensors) case HardwareType.Cpu:
{ cpuHw = hw;
if (sensor.SensorType == SensorType.Load && CacheCpuSensors(hw);
sensor.Name.Contains("CPU Core")) break;
cpuLoadList.Add(sensor);
if (sensor.SensorType == SensorType.Temperature) case HardwareType.GpuNvidia:
{ case HardwareType.GpuAmd:
if (sensor.Name == "Core (Tctl/Tdie)") case HardwareType.GpuIntel:
bestCpuTemp = sensor; gpuHw = hw;
else if (bestCpuTemp == null) CacheGpuSensors(hw);
bestCpuTemp = sensor; break;
}
} case HardwareType.Memory:
memHw = hw;
CacheMemorySensors(hw);
break;
} }
}
}
if (hw.HardwareType == HardwareType.GpuNvidia || private void CacheCpuSensors(IHardware hw)
hw.HardwareType == HardwareType.GpuAmd || {
hw.HardwareType == HardwareType.GpuIntel) var loads = new System.Collections.Generic.List<ISensor>();
foreach (var s in hw.Sensors)
{
if (s.SensorType == SensorType.Load &&
s.Name.Contains("CPU Core"))
loads.Add(s);
if (s.SensorType == SensorType.Temperature)
{ {
foreach (var sensor in hw.Sensors) if (s.Name == "Core (Tctl/Tdie)")
{ cpuTempSensor = s;
if (sensor.SensorType == SensorType.Temperature) else if (cpuTempSensor == null)
{ cpuTempSensor = s;
if (sensor.Name == "GPU Core")
bestGpuTemp = sensor;
else if (bestGpuTemp == null)
bestGpuTemp = sensor;
}
if (sensor.SensorType == SensorType.Load)
{
if (sensor.Name == "D3D 3D")
bestGpu3D = sensor;
else if (bestGpu3D == null && sensor.Name == "GPU Core")
bestGpu3D = sensor;
}
if (sensor.SensorType == SensorType.SmallData)
{
if (sensor.Name == "GPU Memory Used")
bestVramUsed = sensor;
if (sensor.Name == "GPU Memory Total")
bestVramTotal = sensor;
}
}
} }
} }
cpuLoadSensors = cpuLoadList.ToArray(); cpuLoadSensors = loads.ToArray();
cpuTempSensor = bestCpuTemp; }
gpuTempSensor = bestGpuTemp;
gpu3DLoadSensor = bestGpu3D; private void CacheGpuSensors(IHardware hw)
gpuVramUsedSensor = bestVramUsed; {
gpuVramTotalSensor = bestVramTotal; foreach (var s in hw.Sensors)
{
if (s.SensorType == SensorType.Load)
{
if (s.Name == "D3D 3D")
gpu3DLoadSensor = s;
else if (gpu3DLoadSensor == null && s.Name == "GPU Core")
gpu3DLoadSensor = s;
}
if (s.SensorType == SensorType.Temperature)
{
if (s.Name == "GPU Core")
gpuTempSensor = s;
else if (gpuTempSensor == null)
gpuTempSensor = s;
}
if (s.SensorType == SensorType.SmallData)
{
if (s.Name == "GPU Memory Used")
gpuVramUsedSensor = s;
if (s.Name == "GPU Memory Total")
gpuVramTotalSensor = s;
}
}
}
private void CacheMemorySensors(IHardware hw)
{
foreach (var s in hw.Sensors)
{
if (s.SensorType == SensorType.Data && s.Name == "Memory Used")
memUsedSensor = s;
if (s.SensorType == SensorType.Data && s.Name == "Memory Available")
memAvailSensor = s;
}
}
private static string F(float v) => v.ToString("0.###", CI);
public void UpdateAndSend()
{
if (!serial.IsConnected)
{
serial.DiscoverDevice();
return;
}
if (serial.WatchdogExpired)
{
serial.DiscoverDevice();
return;
}
// Update only the hardware we need
cpuHw?.Update();
gpuHw?.Update();
memHw?.Update();
float cpu = GetCpuLoadPercent();
float cpuTemp = GetCpuTemperaturePercent();
float mem = GetMemoryUsagePercent();
float gpu3d = GetGpu3DLoad();
float gpuTemp = GetGpuTemperaturePercent();
float vram = GetGpuVramPercent();
string cmd =
$"SETALL: {F(cpu)},{F(cpuTemp)},{F(mem)},{F(gpu3d)},{F(gpuTemp)},{F(vram)},0,0";
serial.SendSetAll(cmd);
} }
// ---------------- METRICS ---------------- // ---------------- METRICS ----------------
private int GetMemoryUsagePercent() private float GetCpuLoadPercent()
{
float used = 0;
float available = 0;
foreach (var hw in computer.Hardware)
{
if (hw.HardwareType == HardwareType.Memory)
{
hw.Update();
foreach (var sensor in hw.Sensors)
{
if (sensor.SensorType == SensorType.Data && sensor.Name == "Memory Used")
used = sensor.Value ?? 0;
if (sensor.SensorType == SensorType.Data && sensor.Name == "Memory Available")
available = sensor.Value ?? 0;
}
}
}
float total = used + available;
if (total <= 0) return 0;
return (int)Math.Round((used / total) * 100);
}
private int GetCpuLoadPercent()
{ {
if (cpuLoadSensors.Length == 0) return 0; if (cpuLoadSensors.Length == 0) return 0;
float total = 0; float total = 0;
int count = 0; int count = 0;
foreach (var sensor in cpuLoadSensors) foreach (var s in cpuLoadSensors)
{ {
sensor.Hardware.Update(); total += s.Value ?? 0;
total += sensor.Value ?? 0;
count++; count++;
} }
return count == 0 ? 0 : (int)Math.Round(total / count); return count == 0 ? 0 : total / count;
} }
private int GetGpuVramPercent() private float GetCpuTemperaturePercent()
{ {
if (gpuVramUsedSensor == null || gpuVramTotalSensor == null) float t = cpuTempSensor?.Value ?? 0;
return 0; return Math.Clamp(t, 0, 100);
gpuVramUsedSensor.Hardware.Update();
float usedMB = gpuVramUsedSensor.Value ?? 0;
float totalMB = gpuVramTotalSensor.Value ?? 0;
if (totalMB <= 0) return 0;
return (int)Math.Round((usedMB / totalMB) * 100);
} }
private int GetGpu3DLoad() private float GetGpu3DLoad()
{ {
if (gpu3DLoadSensor == null) return 0; return gpu3DLoadSensor?.Value ?? 0;
gpu3DLoadSensor.Hardware.Update();
return (int)Math.Round(gpu3DLoadSensor.Value ?? 0);
} }
private int GetCpuTemperaturePercent() private float GetGpuTemperaturePercent()
{ {
if (cpuTempSensor == null) return 0; float t = gpuTempSensor?.Value ?? 0;
return Math.Clamp(t, 0, 100);
cpuTempSensor.Hardware.Update();
float temp = cpuTempSensor.Value ?? 0;
return (int)Math.Round(Math.Clamp(temp, 0, 100));
} }
private int GetGpuTemperaturePercent() private float GetGpuVramPercent()
{ {
if (gpuTempSensor == null) return 0; float used = gpuVramUsedSensor?.Value ?? 0;
float total = gpuVramTotalSensor?.Value ?? 0;
gpuTempSensor.Hardware.Update(); if (total <= 0) return 0;
float temp = gpuTempSensor.Value ?? 0; return (used / total) * 100f;
return (int)Math.Round(Math.Clamp(temp, 0, 100));
} }
// ---------------- OSC ---------------- private float GetMemoryUsagePercent()
private void SendOscBundle(
float cpu, float cpuTemp, float mem,
float gpu3d, float gpuTemp, float vram)
{ {
var messages = new List<byte[]>(); float used = memUsedSensor?.Value ?? 0;
float avail = memAvailSensor?.Value ?? 0;
messages.Add(BuildOscFloatMessage("/cpu", cpu)); float total = used + avail;
messages.Add(BuildOscFloatMessage("/cputemp", cpuTemp)); if (total <= 0) return 0;
messages.Add(BuildOscFloatMessage("/memory", mem));
messages.Add(BuildOscFloatMessage("/gpu3d", gpu3d));
messages.Add(BuildOscFloatMessage("/gputemp", gpuTemp));
messages.Add(BuildOscFloatMessage("/vram", vram));
byte[] bundle = BuildOscBundle(messages); return (used / total) * 100f;
udp.Send(bundle, bundle.Length, oscIp, oscPort);
} }
private byte[] BuildOscBundle(List<byte[]> messages) public void Dispose()
{ {
List<byte[]> parts = new List<byte[]>(); serial.Dispose();
computer.Close();
parts.Add(PadOscString("#bundle"));
byte[] timetag = new byte[8];
timetag[7] = 1;
parts.Add(timetag);
foreach (var msg in messages)
{
byte[] len = BitConverter.GetBytes(msg.Length);
if (BitConverter.IsLittleEndian)
Array.Reverse(len);
parts.Add(len);
parts.Add(msg);
}
int total = 0;
foreach (var p in parts)
total += p.Length;
byte[] bundle = new byte[total];
int offset = 0;
foreach (var p in parts)
{
Buffer.BlockCopy(p, 0, bundle, offset, p.Length);
offset += p.Length;
}
return bundle;
}
private byte[] BuildOscFloatMessage(string address, float value)
{
byte[] addr = PadOscString(address);
byte[] types = PadOscString(",f");
byte[] floatBytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
Array.Reverse(floatBytes);
byte[] packet = new byte[addr.Length + types.Length + floatBytes.Length];
Buffer.BlockCopy(addr, 0, packet, 0, addr.Length);
Buffer.BlockCopy(types, 0, packet, addr.Length, types.Length);
Buffer.BlockCopy(floatBytes, 0, packet, addr.Length + types.Length, floatBytes.Length);
return packet;
}
private byte[] PadOscString(string s)
{
byte[] raw = Encoding.ASCII.GetBytes(s);
int paddedLength = ((raw.Length + 1 + 3) / 4) * 4;
byte[] padded = new byte[paddedLength];
Buffer.BlockCopy(raw, 0, padded, 0, raw.Length);
return padded;
} }
} }

View File

@@ -1,86 +1,41 @@
#nullable enable
using System; using System;
using System.Drawing; using System.Drawing;
using System.Reflection;
using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
namespace analog_system_monitor public class TrayApp : ApplicationContext
{ {
public class TrayApp : ApplicationContext private NotifyIcon trayIcon;
private Telemetry telemetry;
public TrayApp()
{ {
private NotifyIcon trayIcon; telemetry = new Telemetry();
private Thread workerThread; telemetry.Initialize();
private bool running = true;
public TrayApp() trayIcon = new NotifyIcon()
{ {
trayIcon = new NotifyIcon() Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath),
{ Visible = true,
Icon = LoadEmbeddedIcon(), Text = "Telemetry Running"
Text = "Analog System Monitor", };
Visible = true,
ContextMenuStrip = BuildMenu()
};
workerThread = new Thread(WorkerLoop) var menu = new ContextMenuStrip();
{ menu.Items.Add("Exit", null, OnExit);
IsBackground = true trayIcon.ContextMenuStrip = menu;
};
workerThread.Start();
}
private Icon LoadEmbeddedIcon() // Start telemetry loop
{ var timer = new System.Windows.Forms.Timer();
var assembly = Assembly.GetExecutingAssembly(); timer.Interval = 1000;
var resourceName = "analog_system_monitor.telemetry_icon.ico"; timer.Tick += (s, e) => telemetry.UpdateAndSend();
timer.Start();
}
using var stream = assembly.GetManifestResourceStream(resourceName); private void OnExit(object? sender, EventArgs e)
if (stream == null) {
{ telemetry.Dispose();
MessageBox.Show("Embedded icon not found: " + resourceName); trayIcon.Visible = false;
return SystemIcons.Application; Application.Exit();
}
return new Icon(stream);
}
private ContextMenuStrip BuildMenu()
{
var menu = new ContextMenuStrip();
var exitItem = new ToolStripMenuItem("Exit");
exitItem.Click += (s, e) => ExitApp();
menu.Items.Add(exitItem);
return menu;
}
private void WorkerLoop()
{
var telemetry = new Telemetry();
telemetry.Initialize();
while (running)
{
telemetry.UpdateAndSend();
Thread.Sleep(telemetry.UpdateRateMs);
}
}
private void ExitApp()
{
running = false;
try
{
workerThread?.Join(500);
}
catch { }
trayIcon.Visible = false;
trayIcon.Dispose();
Application.Exit();
}
} }
} }

View File

@@ -4,20 +4,29 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework> <TargetFramework>net10.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <!-- Single-file EXE -->
<PublishSingleFile>true</PublishSingleFile> <PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<DebugType>None</DebugType> <!-- No trimming (LHM + reflection will break) -->
<DebugSymbols>false</DebugSymbols> <PublishTrimmed>false</PublishTrimmed>
<ApplicationIcon>telemetry_icon.ico</ApplicationIcon>
<!-- No compression (avoids startup delays + debugging issues) -->
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
<!-- Keep debugging symbols optional -->
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<ApplicationIcon>telemetry_icon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.5" /> <PackageReference Include="LibreHardwareMonitorLib" Version="0.9.5" />
<None Include="telemetry_icon.ico" /> <None Include="telemetry_icon.ico" />
<EmbeddedResource Include="telemetry_icon.ico" /> <EmbeddedResource Include="telemetry_icon.ico" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="AnalogSystemMonitor"
type="win32"
/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<!-- Force the EXE to always run as administrator -->
<requestedExecutionLevel
level="requireAdministrator"
uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>

View File

@@ -0,0 +1,53 @@
# ============================================
# Build Release Single-File EXE for Analog System Monitor
# Output directory: ./release/
# ============================================
Write-Host "=== Analog System Monitor Release Build ===" -ForegroundColor Cyan
# Ensure script runs from project directory
$project = "analog_system_monitor.csproj"
if (!(Test-Path $project)) {
Write-Host "Error: Telemetry.csproj not found in this directory." -ForegroundColor Red
exit 1
}
# Clean previous builds
Write-Host "Cleaning previous build artifacts..."
dotnet clean -c Release
# Publish using settings from the .csproj
Write-Host "Publishing Release build..."
dotnet publish -c Release
# Determine publish folder
$publishDir = Join-Path "bin" "Release\net10.0-windows\win-x64\publish"
if (!(Test-Path $publishDir)) {
Write-Host "Error: Publish directory not found." -ForegroundColor Red
exit 1
}
# Custom output directory
$releaseDir = Join-Path (Get-Location) "release"
# Create directory if missing
if (!(Test-Path $releaseDir)) {
Write-Host "Creating release directory..."
New-Item -ItemType Directory -Path $releaseDir | Out-Null
}
# Copy all published files to ./release/
Write-Host "Copying published files to ./release/ ..."
Copy-Item -Path "$publishDir\*" -Destination $releaseDir -Recurse -Force
Write-Host "Build completed successfully!" -ForegroundColor Green
Write-Host "Release output: $releaseDir"
# List produced files
Write-Host "`nRelease files:"
Get-ChildItem $releaseDir | Format-Table Name, Length
# Open folder in Explorer
Write-Host "`nOpening release folder..."
Start-Process explorer.exe $releaseDir

View File

@@ -1,51 +0,0 @@
# ============================================
# Analog System Monitor Release Script
# Creates a clean, single-file Windows build
# ============================================
param(
[string]$Version = "1.0.0"
)
$ErrorActionPreference = "Stop"
Write-Host "=== Analog System Monitor Release Script ==="
Write-Host "Version: $Version"
Write-Host ""
# Paths
$project = "analog_system_monitor.csproj"
$releaseDir = "release\$Version"
# Clean old release
if (Test-Path $releaseDir) {
Write-Host "Cleaning old release directory..."
Remove-Item -Recurse -Force $releaseDir
}
# Ensure release directory exists
New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null
Write-Host "Restoring packages..."
dotnet restore $project
Write-Host "Cleaning project..."
dotnet clean $project -c Release
Write-Host "Publishing single-file executable..."
dotnet publish $project `
-c Release `
-r win-x64 `
--self-contained true `
/p:PublishSingleFile=true `
/p:IncludeNativeLibrariesForSelfExtract=true `
/p:DebugType=None `
/p:DebugSymbols=false `
/p:Version=$Version `
-o "$releaseDir"
Write-Host ""
Write-Host "============================================"
Write-Host " Release build completed successfully"
Write-Host " Output folder: $releaseDir"
Write-Host "============================================"