From a9957bc695ff6d87671beaa75fa2fdf4ce6fdb2b Mon Sep 17 00:00:00 2001 From: Robin Cerny Date: Sun, 18 Jan 2026 05:41:51 +0100 Subject: [PATCH] switched back to ESP32 and UDP, but without OSC, because serial under windows is a bitch --- .../analog_system_monitor_arduino.ino | 208 ++++++++++-------- analog_system_monitor_dotnet/SerialManager.cs | 158 ------------- analog_system_monitor_dotnet/Telemetry.cs | 50 ++--- analog_system_monitor_dotnet/TrayApp.cs | 3 +- analog_system_monitor_dotnet/UdpSender.cs | 64 ++++++ .../analog_system_monitor.csproj | 4 +- 6 files changed, 202 insertions(+), 285 deletions(-) delete mode 100644 analog_system_monitor_dotnet/SerialManager.cs create mode 100644 analog_system_monitor_dotnet/UdpSender.cs diff --git a/analog_system_monitor_arduino/analog_system_monitor_arduino.ino b/analog_system_monitor_arduino/analog_system_monitor_arduino.ino index b2342d2..6dee976 100644 --- a/analog_system_monitor_arduino/analog_system_monitor_arduino.ino +++ b/analog_system_monitor_arduino/analog_system_monitor_arduino.ino @@ -1,24 +1,42 @@ #include -#include +#include +#include +#include // ------------------------------- // Firmware version // ------------------------------- -const char* FIRMWARE_VERSION = "V1.0"; +const char* FIRMWARE_VERSION = "V2.4_UDP_LEDC_WM_SLEW"; // ------------------------------- -// PWM setup +// UDP // ------------------------------- +WiFiUDP udp; +const int listenPort = 12345; // Must match PC config.json +// ------------------------------- +// PWM setup (LEDC, ESP32 Core 3.x) +// ------------------------------- const uint8_t NUM_CHANNELS = 8; -uint8_t pwmPins[NUM_CHANNELS] = {14, 15, 26, 27, 8, 7, 6, 5}; -const uint32_t pwmFrequency = 10000; +uint8_t pwmPins[NUM_CHANNELS] = { + 26, // D0 + 22, // D1 + 21, // D2 + 17, // D3 + 16, // D4 + 5, // D5 + 18, // D6 + 19 // D7 + // 23 (D8) remains as a spare +}; + +const uint32_t pwmFrequency = 25000; // 25 kHz +const uint8_t pwmResolution = 10; // 10-bit resolution (0–1023) // ------------------------------- // Calibration tables // ------------------------------- - float logicalPoints[5] = {0, 25, 50, 75, 100}; float calibratedPoints[NUM_CHANNELS][5] = { @@ -33,24 +51,25 @@ float calibratedPoints[NUM_CHANNELS][5] = { }; // ------------------------------- -// Duty tracking +// Duty tracking + Slew system // ------------------------------- - float currentDuty[NUM_CHANNELS] = {0.0f}; -RP2040_PWM* pwm[NUM_CHANNELS]; +float targetDuty[NUM_CHANNELS] = {0.0f}; +float slewStartDuty[NUM_CHANNELS] = {0.0f}; + +unsigned long slewStartTime = 0; +const unsigned long slewDuration = 1000; // 1 second smooth transition // ------------------------------- -// Watchdog +// Watchdog (UDP-based) // ------------------------------- - -unsigned long lastSerialTime = 0; +unsigned long lastPacketTime = 0; const unsigned long watchdogTimeout = 5000; // 5 seconds unsigned long lastFadeTime = 0; // ------------------------------- // Calibration interpolation // ------------------------------- - float applyCalibration(uint8_t ch, float logicalDuty) { if (logicalDuty <= 0.0f) return calibratedPoints[ch][0]; if (logicalDuty >= 100.0f) return calibratedPoints[ch][4]; @@ -71,105 +90,116 @@ float applyCalibration(uint8_t ch, float logicalDuty) { // ------------------------------- // Setup // ------------------------------- - void setup() { Serial.begin(115200); delay(300); + Serial.println("Booting Analog System Monitor (UDP + LEDC + WiFiManager + Slew)"); + Serial.print("Firmware: "); + Serial.println(FIRMWARE_VERSION); + + // LEDC PWM init (ESP32 Core 3.x API) for (int ch = 0; ch < NUM_CHANNELS; ch++) { - pwm[ch] = new RP2040_PWM(pwmPins[ch], pwmFrequency, 0.0f); - if (pwm[ch]) pwm[ch]->setPWM(); + bool ok = ledcAttach(pwmPins[ch], pwmFrequency, pwmResolution); + if (!ok) { + Serial.print("LEDC attach failed on pin "); + Serial.println(pwmPins[ch]); + } + ledcWrite(pwmPins[ch], 0); // duty = 0% } - lastSerialTime = millis(); + // ------------------------------- + // WiFi Manager (Captive Portal) + // ------------------------------- + WiFiManager wm; + + wm.setHostname("AnalogMonitor"); + wm.setTimeout(180); // 3 minutes before giving up + + Serial.println("Starting WiFiManager..."); + + bool res = wm.autoConnect("AnalogMonitor-Setup"); + + if (!res) { + Serial.println("WiFi failed or timed out. Rebooting..."); + delay(2000); + ESP.restart(); + } + + Serial.println("WiFi connected!"); + Serial.print("IP: "); + Serial.println(WiFi.localIP()); + + // UDP init + udp.begin(listenPort); + Serial.print("Listening on UDP port "); + Serial.println(listenPort); + + lastPacketTime = millis(); } // ------------------------------- // Loop // ------------------------------- - void loop() { - // -------- Serial parsing -------- - while (Serial.available()) { - String s = Serial.readStringUntil('\n'); - s.trim(); + // -------- UDP parsing -------- + int packetSize = udp.parsePacket(); + if (packetSize > 0) { + char buf[256]; + int len = udp.read(buf, sizeof(buf) - 1); + buf[len] = '\0'; - // --- Device identification command --- - if (s.equalsIgnoreCase("PING")) { - Serial.print("Analog_System_Monitor_"); - Serial.println(FIRMWARE_VERSION); - lastSerialTime = millis(); - continue; + float values[NUM_CHANNELS] = {0}; + int idx = 0; + + char* token = strtok(buf, ","); + while (token != nullptr && idx < NUM_CHANNELS) { + values[idx] = atof(token); + idx++; + token = strtok(nullptr, ","); } - // --- Batch update command --- - if (s.startsWith("SETALL:")) { - String payload = s.substring(7); - payload.trim(); + if (idx == NUM_CHANNELS) { - 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 + // Start a new slew toward the target for (int ch = 0; ch < NUM_CHANNELS; ch++) { - currentDuty[ch] = newValues[ch]; - float calibratedDuty = applyCalibration(ch, currentDuty[ch]); - pwm[ch]->setPWM(pwmPins[ch], pwmFrequency, calibratedDuty); + targetDuty[ch] = values[ch]; + slewStartDuty[ch] = currentDuty[ch]; } + slewStartTime = millis(); - Serial.println("OK"); - lastSerialTime = millis(); - continue; - } + lastPacketTime = millis(); - // --- Unknown command --- - if (s.length() > 0) { - Serial.println("ERROR"); + // Debug output + Serial.println("Received UDP packet:"); + for (int i = 0; i < NUM_CHANNELS; i++) { + Serial.print(" CH"); + Serial.print(i); + Serial.print(": "); + Serial.println(values[i]); + } + Serial.println(); } } - // -------- Watchdog fade-to-zero (time-based exponential) -------- - if (millis() - lastSerialTime > watchdogTimeout) { + // -------- Slew-rate limiting (smooth 1-second transitions) -------- + unsigned long now = millis(); + float progress = (float)(now - slewStartTime) / (float)slewDuration; + if (progress > 1.0f) progress = 1.0f; + + for (int ch = 0; ch < NUM_CHANNELS; ch++) { + float newDuty = slewStartDuty[ch] + (targetDuty[ch] - slewStartDuty[ch]) * progress; + currentDuty[ch] = newDuty; + + float calibratedDuty = applyCalibration(ch, newDuty); + int duty = (int)((calibratedDuty / 100.0f) * ((1 << pwmResolution) - 1)); + + ledcWrite(pwmPins[ch], duty); + } + + // -------- Watchdog fade-to-zero (UDP-based) -------- + if (millis() - lastPacketTime > watchdogTimeout) { const unsigned long fadeInterval = 1; @@ -187,7 +217,9 @@ void loop() { currentDuty[ch] = 0.0f; float calibratedDuty = applyCalibration(ch, currentDuty[ch]); - pwm[ch]->setPWM(pwmPins[ch], pwmFrequency, calibratedDuty); + int duty = (int)((calibratedDuty / 100.0f) * ((1 << pwmResolution) - 1)); + + ledcWrite(pwmPins[ch], duty); } } } diff --git a/analog_system_monitor_dotnet/SerialManager.cs b/analog_system_monitor_dotnet/SerialManager.cs deleted file mode 100644 index 65937a9..0000000 --- a/analog_system_monitor_dotnet/SerialManager.cs +++ /dev/null @@ -1,158 +0,0 @@ -#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(); - } -} diff --git a/analog_system_monitor_dotnet/Telemetry.cs b/analog_system_monitor_dotnet/Telemetry.cs index 415ea29..026ee34 100644 --- a/analog_system_monitor_dotnet/Telemetry.cs +++ b/analog_system_monitor_dotnet/Telemetry.cs @@ -9,15 +9,13 @@ public class Telemetry : IDisposable private const int UpdateRateDefaultMs = 1000; public int UpdateRateMs => UpdateRateDefaultMs; - private readonly SerialManager serial = new SerialManager(); + private readonly UdpSender udp = new UdpSender(); 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(); private ISensor? cpuTempSensor; @@ -33,22 +31,11 @@ public class Telemetry : IDisposable public void Initialize() { - serial.DiscoverDevice(); - - // Enable only what we need computer.IsCpuEnabled = true; computer.IsGpuEnabled = true; computer.IsMemoryEnabled = true; - computer.IsMotherboardEnabled = false; - computer.IsControllerEnabled = false; - computer.IsNetworkEnabled = false; - computer.IsStorageEnabled = false; - computer.IsBatteryEnabled = false; - computer.IsPsuEnabled = false; - computer.Open(); - CacheHardwareAndSensors(); } @@ -145,23 +132,8 @@ public class Telemetry : IDisposable } } - 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(); @@ -173,14 +145,22 @@ public class Telemetry : IDisposable float gpuTemp = GetGpuTemperaturePercent(); float vram = GetGpuVramPercent(); - string cmd = - $"SETALL: {F(cpu)},{F(cpuTemp)},{F(mem)},{F(gpu3d)},{F(gpuTemp)},{F(vram)},0,0"; + // Prepare 8 floats (future‑proof) + float[] packet = + { + cpu, + cpuTemp, + mem, + gpu3d, + gpuTemp, + vram, + 0f, // reserved for future use + 0f // reserved for future use + }; - serial.SendSetAll(cmd); + udp.SendFloats(packet); } - // ---------------- METRICS ---------------- - private float GetCpuLoadPercent() { if (cpuLoadSensors.Length == 0) return 0; @@ -236,7 +216,7 @@ public class Telemetry : IDisposable public void Dispose() { - serial.Dispose(); + udp.Dispose(); computer.Close(); } } diff --git a/analog_system_monitor_dotnet/TrayApp.cs b/analog_system_monitor_dotnet/TrayApp.cs index 5c0d8ed..3582d83 100644 --- a/analog_system_monitor_dotnet/TrayApp.cs +++ b/analog_system_monitor_dotnet/TrayApp.cs @@ -18,14 +18,13 @@ public class TrayApp : ApplicationContext { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath), Visible = true, - Text = "Telemetry Running" + Text = "Telemetry Running (UDP)" }; var menu = new ContextMenuStrip(); menu.Items.Add("Exit", null, OnExit); trayIcon.ContextMenuStrip = menu; - // Start telemetry loop var timer = new System.Windows.Forms.Timer(); timer.Interval = 1000; timer.Tick += (s, e) => telemetry.UpdateAndSend(); diff --git a/analog_system_monitor_dotnet/UdpSender.cs b/analog_system_monitor_dotnet/UdpSender.cs new file mode 100644 index 0000000..6779784 --- /dev/null +++ b/analog_system_monitor_dotnet/UdpSender.cs @@ -0,0 +1,64 @@ +#nullable enable + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text.Json; + +public class UdpSender : IDisposable +{ + private readonly UdpClient client = new UdpClient(); + private IPEndPoint endpoint; + + private const string DefaultIp = "192.168.1.50"; + private const int DefaultPort = 12345; + + public UdpSender() + { + string exeDir = AppContext.BaseDirectory; + string cfgPath = Path.Combine(exeDir, "config.json"); + + // Create default config if missing + if (!File.Exists(cfgPath)) + { + var defaultCfg = new UdpConfig + { + esp32_ip = DefaultIp, + esp32_port = DefaultPort + }; + + string json = JsonSerializer.Serialize( + defaultCfg, + new JsonSerializerOptions { WriteIndented = true } + ); + + File.WriteAllText(cfgPath, json); + } + + // Load config + var jsonText = File.ReadAllText(cfgPath); + var cfg = JsonSerializer.Deserialize(jsonText) + ?? throw new Exception("Invalid config.json"); + + endpoint = new IPEndPoint(IPAddress.Parse(cfg.esp32_ip), cfg.esp32_port); + } + + public void SendFloats(float[] values) + { + string packet = string.Join(",", values); + byte[] data = System.Text.Encoding.ASCII.GetBytes(packet); + client.Send(data, data.Length, endpoint); + } + + public void Dispose() + { + client.Dispose(); + } + + private class UdpConfig + { + public string esp32_ip { get; set; } = DefaultIp; + public int esp32_port { get; set; } = DefaultPort; + } +} diff --git a/analog_system_monitor_dotnet/analog_system_monitor.csproj b/analog_system_monitor_dotnet/analog_system_monitor.csproj index e208a14..5c2ae0c 100644 --- a/analog_system_monitor_dotnet/analog_system_monitor.csproj +++ b/analog_system_monitor_dotnet/analog_system_monitor.csproj @@ -12,8 +12,8 @@ false - - false + true + true none