Compare commits
20 Commits
d432db9985
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 42e49b6c06 | |||
| 153d134f88 | |||
| d3586e697b | |||
| 9312ceb22b | |||
| 0197266418 | |||
| 48059da373 | |||
| 536ecaa7d9 | |||
| 60f12e22e4 | |||
| 31dcc5f502 | |||
| 49daa09aa6 | |||
| e634563cdd | |||
| 3fc15c85d3 | |||
| 81d6bced05 | |||
| ad66265260 | |||
| 9fa0e4466c | |||
| f21269daa5 | |||
| b8d5e25352 | |||
| c1d7ba4b3d | |||
| a9957bc695 | |||
| 2311647885 |
54
analog_system_monitor_arduino/Command_Summary.txt
Normal file
54
analog_system_monitor_arduino/Command_Summary.txt
Normal 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
|
||||||
105
analog_system_monitor_arduino/Config.h
Normal file
105
analog_system_monitor_arduino/Config.h
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Firmware version
|
||||||
|
// -------------------------------
|
||||||
|
static const char* FIRMWARE_VERSION = "V2.4_UDP_LEDC_WM_SLEW";
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// UDP
|
||||||
|
// -------------------------------
|
||||||
|
static const int listenPort = 12345; // default / legacy constant
|
||||||
|
static const unsigned long watchdogTimeout = 5000; // 5 seconds
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// PWM setup (LEDC, ESP32 Core 3.x)
|
||||||
|
// -------------------------------
|
||||||
|
static const uint8_t NUM_CHANNELS = 8;
|
||||||
|
|
||||||
|
static const uint8_t pwmPins[NUM_CHANNELS] = {
|
||||||
|
26, // D0
|
||||||
|
22, // D1
|
||||||
|
21, // D2
|
||||||
|
17, // D3
|
||||||
|
16, // D4
|
||||||
|
5, // D5
|
||||||
|
18, // D6
|
||||||
|
19 // D7
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint32_t pwmFrequency = 25000; // 25 kHz
|
||||||
|
static const uint8_t pwmResolution = 10; // 10-bit resolution (0–1023)
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Channel labels for debugging
|
||||||
|
// -------------------------------
|
||||||
|
static const char* const channelLabels[NUM_CHANNELS] = {
|
||||||
|
"CPU Load",
|
||||||
|
"CPU Temp",
|
||||||
|
"RAM Usage",
|
||||||
|
"GPU Load",
|
||||||
|
"GPU Temp",
|
||||||
|
"VRAM Usage",
|
||||||
|
"Reserved 6",
|
||||||
|
"Reserved 7"
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char* const channelUnits[NUM_CHANNELS] = {
|
||||||
|
"%", // CPU Load
|
||||||
|
"°C", // CPU Temp
|
||||||
|
"%", // RAM Usage
|
||||||
|
"%", // GPU Load
|
||||||
|
"°C", // GPU Temp
|
||||||
|
"%", // VRAM Usage
|
||||||
|
"", // Reserved
|
||||||
|
"" // Reserved
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Channel labels ESPUI
|
||||||
|
// -------------------------------
|
||||||
|
static const char* const channelDropdownLabels[NUM_CHANNELS] = {
|
||||||
|
"CH0 (CPU Load)",
|
||||||
|
"CH1 (CPU Temp)",
|
||||||
|
"CH2 (RAM Usage)",
|
||||||
|
"CH3 (GPU Load)",
|
||||||
|
"CH4 (GPU Temp)",
|
||||||
|
"CH5 (VRAM Usage)",
|
||||||
|
"CH6 (Reserved 6)",
|
||||||
|
"CH7 (Reserved 7)"
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Calibration logical points
|
||||||
|
// -------------------------------
|
||||||
|
static const unsigned long slewDuration = 1000; // 1 second smooth transition
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Animation tuning
|
||||||
|
// -------------------------------
|
||||||
|
static const float FADE_IN_FACTOR = 0.998f; // boot-up 0 → 100%
|
||||||
|
static const float FADE_OUT_FACTOR = 0.999f; // watchdog 100% → 0
|
||||||
|
static const unsigned long FADE_INTERVAL = 1; // ms between fade steps
|
||||||
|
// Lighting fade durations (milliseconds)
|
||||||
|
#define LIGHTING_FADE_IN_DURATION 1250
|
||||||
|
#define LIGHTING_FADE_OUT_DURATION 4250
|
||||||
|
// Clamping brightness at the low end
|
||||||
|
const uint8_t BRIGHTNESS_MIN_VISIBLE = 33;
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Lighting (FastLED)
|
||||||
|
// -------------------------------
|
||||||
|
#define LED_PIN 23
|
||||||
|
#define NUM_LEDS 20
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Connection state machine
|
||||||
|
// -------------------------------
|
||||||
|
enum ConnectionState {
|
||||||
|
STATE_DISCONNECTED, // fade to zero
|
||||||
|
STATE_CONNECTING, // fade in 0 → 100%
|
||||||
|
STATE_WAIT_FOR_FIRST_PACKET, // hold at 100%, wait for first UDP
|
||||||
|
STATE_CONNECTED // normal UDP-driven slew
|
||||||
|
};
|
||||||
438
analog_system_monitor_arduino/Core.cpp
Normal file
438
analog_system_monitor_arduino/Core.cpp
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "Core.h"
|
||||||
|
#include <FastLED.h>
|
||||||
|
|
||||||
|
WiFiUDP udp;
|
||||||
|
Preferences prefs;
|
||||||
|
int udpPort = listenPort;
|
||||||
|
|
||||||
|
float currentDuty[NUM_CHANNELS] = {0.0f};
|
||||||
|
float targetDuty[NUM_CHANNELS] = {0.0f};
|
||||||
|
float slewStartDuty[NUM_CHANNELS] = {0.0f};
|
||||||
|
|
||||||
|
unsigned long slewStartTime = 0;
|
||||||
|
unsigned long lastPacketTime = 0;
|
||||||
|
unsigned long lastFadeTime = 0;
|
||||||
|
|
||||||
|
float logicalPoints[5] = {0, 25, 50, 75, 100};
|
||||||
|
|
||||||
|
float calibratedPoints[NUM_CHANNELS][5] = {
|
||||||
|
{0.0f, 25.0f, 50.0f, 75.0f, 99.0f},
|
||||||
|
{0.0f, 24.0f, 49.0f, 74.0f, 98.0f},
|
||||||
|
{0.0f, 26.0f, 51.0f, 76.0f, 99.0f},
|
||||||
|
{0.0f, 25.0f, 50.0f, 75.0f, 97.0f},
|
||||||
|
{0.0f, 25.0f, 50.0f, 75.0f, 99.0f},
|
||||||
|
{0.0f, 24.0f, 50.0f, 74.0f, 98.0f},
|
||||||
|
{0.0f, 25.0f, 49.0f, 75.0f, 97.0f},
|
||||||
|
{0.0f, 26.0f, 50.0f, 76.0f, 99.0f}
|
||||||
|
};
|
||||||
|
|
||||||
|
CRGB leds[NUM_LEDS];
|
||||||
|
|
||||||
|
uint8_t lightingHue = 0;
|
||||||
|
uint8_t lightingSaturation = 255;
|
||||||
|
uint8_t lightingBrightness = 255;
|
||||||
|
uint8_t lightingBrightnessSaved = 255;
|
||||||
|
|
||||||
|
// Lighting fade state
|
||||||
|
bool lightingFading = false;
|
||||||
|
uint8_t lightingFadeStart = 0;
|
||||||
|
uint8_t lightingFadeEnd = 0;
|
||||||
|
unsigned long lightingFadeStartTime = 0;
|
||||||
|
unsigned long lightingFadeDuration = 0;
|
||||||
|
|
||||||
|
bool overrideActive[NUM_CHANNELS] = {false};
|
||||||
|
|
||||||
|
ConnectionState connectionState = STATE_DISCONNECTED;
|
||||||
|
|
||||||
|
// UI IDs (defined here, used also by UI.cpp)
|
||||||
|
uint16_t connectionStatusLabel;
|
||||||
|
uint16_t calChannelDropdown;
|
||||||
|
uint16_t calInputs[5];
|
||||||
|
uint16_t calTestValueInput;
|
||||||
|
uint16_t calSaveButton;
|
||||||
|
uint16_t calOverrideSwitch;
|
||||||
|
uint8_t selectedCalChannel = 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];
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (logicalDuty >= logicalPoints[i] && logicalDuty <= logicalPoints[i+1]) {
|
||||||
|
float x0 = logicalPoints[i];
|
||||||
|
float x1 = logicalPoints[i+1];
|
||||||
|
float y0 = calibratedPoints[ch][i];
|
||||||
|
float y1 = calibratedPoints[ch][i+1];
|
||||||
|
float t = (logicalDuty - x0) / (x1 - x0);
|
||||||
|
return y0 + t * (y1 - y0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calibratedPoints[ch][4];
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyLighting() {
|
||||||
|
// Convert HSV (0–255 each) to RGB
|
||||||
|
CHSV hsv(lightingHue, lightingSaturation, lightingBrightness);
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_LEDS; i++) {
|
||||||
|
leds[i] = hsv;
|
||||||
|
}
|
||||||
|
|
||||||
|
FastLED.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void startLightingFade(uint8_t from, uint8_t to, unsigned long duration) {
|
||||||
|
lightingFadeStart = from;
|
||||||
|
lightingFadeEnd = to;
|
||||||
|
lightingFadeDuration = duration;
|
||||||
|
lightingFadeStartTime = millis();
|
||||||
|
lightingFading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateConnectionStatusUI(ConnectionState state) {
|
||||||
|
const char* text = "Unknown";
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case STATE_DISCONNECTED:
|
||||||
|
text = "Disconnected";
|
||||||
|
break;
|
||||||
|
case STATE_CONNECTING:
|
||||||
|
text = "Connecting...";
|
||||||
|
break;
|
||||||
|
case STATE_WAIT_FOR_FIRST_PACKET:
|
||||||
|
text = "Waiting for first UDP packet";
|
||||||
|
break;
|
||||||
|
case STATE_CONNECTED:
|
||||||
|
text = "Connected";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESPUI.updateControlValue(connectionStatusLabel, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Core init (called from setup)
|
||||||
|
// -------------------------------
|
||||||
|
void coreInit() {
|
||||||
|
// LEDC PWM init
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
bool ok = ledcAttach(pwmPins[ch], pwmFrequency, pwmResolution);
|
||||||
|
if (!ok) {
|
||||||
|
Serial.print("LEDC attach failed on pin ");
|
||||||
|
Serial.println(pwmPins[ch]);
|
||||||
|
}
|
||||||
|
ledcWrite(pwmPins[ch], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preferences: load UDP port (default = listenPort)
|
||||||
|
prefs.begin("analogmon", false);
|
||||||
|
udpPort = prefs.getInt("udpPort", listenPort);
|
||||||
|
|
||||||
|
// Load calibration from flash (if present)
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
String key = "cal_" + String(ch) + "_" + String(i);
|
||||||
|
float val = prefs.getFloat(key.c_str(), calibratedPoints[ch][i]);
|
||||||
|
calibratedPoints[ch][i] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lightingHue = prefs.getUChar("light_hue", 0);
|
||||||
|
lightingSaturation = prefs.getUChar("light_sat", 255);
|
||||||
|
lightingBrightnessSaved = prefs.getUChar("light_bright",255);
|
||||||
|
lightingBrightness = lightingBrightnessSaved;
|
||||||
|
|
||||||
|
|
||||||
|
Serial.printf("Lighting loaded (0–255): H=%d S=%d B=%d\n",
|
||||||
|
lightingHue, lightingSaturation, lightingBrightness);
|
||||||
|
|
||||||
|
// FastLED init
|
||||||
|
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
|
||||||
|
FastLED.setBrightness(255); // full brightness; HSV V controls actual output
|
||||||
|
|
||||||
|
// Apply lighting immediately at boot
|
||||||
|
applyLighting();
|
||||||
|
|
||||||
|
// Start UDP with runtime port
|
||||||
|
udp.begin(udpPort);
|
||||||
|
Serial.print("Listening on UDP port ");
|
||||||
|
Serial.println(udpPort);
|
||||||
|
|
||||||
|
lastPacketTime = millis();
|
||||||
|
connectionState = STATE_DISCONNECTED;
|
||||||
|
updateConnectionStatusUI(connectionState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// UDP parsing
|
||||||
|
// -------------------------------
|
||||||
|
void coreHandleUDP() {
|
||||||
|
int packetSize = udp.parsePacket();
|
||||||
|
if (packetSize <= 0) return;
|
||||||
|
|
||||||
|
char buf[256];
|
||||||
|
int len = udp.read(buf, sizeof(buf) - 1);
|
||||||
|
buf[len] = '\0';
|
||||||
|
|
||||||
|
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, ",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx != NUM_CHANNELS) return;
|
||||||
|
|
||||||
|
if (connectionState == STATE_DISCONNECTED) {
|
||||||
|
// First valid packet after being disconnected → start CONNECTING
|
||||||
|
Serial.println("STATE CHANGE: DISCONNECTED → CONNECTING (UDP connection established)");
|
||||||
|
connectionState = STATE_CONNECTING;
|
||||||
|
updateConnectionStatusUI(connectionState);
|
||||||
|
startLightingFade(0, lightingBrightnessSaved, LIGHTING_FADE_IN_DURATION);
|
||||||
|
|
||||||
|
// Initialize fade-in: start from 0 on all non-override channels
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
if (!overrideActive[ch]) {
|
||||||
|
currentDuty[ch] = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPacketTime = millis(); // prevent watchdog during fade-in
|
||||||
|
}
|
||||||
|
else if (connectionState == STATE_CONNECTING) {
|
||||||
|
// Ignore UDP values during fade-in, just keep watchdog alive
|
||||||
|
lastPacketTime = millis();
|
||||||
|
}
|
||||||
|
else if (connectionState == STATE_WAIT_FOR_FIRST_PACKET) {
|
||||||
|
// First real packet after fade-in completes
|
||||||
|
Serial.println("STATE CHANGE: WAIT_FOR_FIRST_PACKET → CONNECTED (first UDP packet received)");
|
||||||
|
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
if (!overrideActive[ch]) {
|
||||||
|
targetDuty[ch] = values[ch];
|
||||||
|
slewStartDuty[ch] = currentDuty[ch]; // currently ~100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slewStartTime = millis();
|
||||||
|
lastPacketTime = millis();
|
||||||
|
connectionState = STATE_CONNECTED;
|
||||||
|
updateConnectionStatusUI(connectionState);
|
||||||
|
|
||||||
|
// Debug output
|
||||||
|
Serial.println("Received UDP packet (first after fade-in):");
|
||||||
|
for (int i = 0; i < NUM_CHANNELS; i++) {
|
||||||
|
Serial.print(" CH");
|
||||||
|
Serial.print(i);
|
||||||
|
Serial.print(" (");
|
||||||
|
Serial.print(channelLabels[i]);
|
||||||
|
Serial.print("): ");
|
||||||
|
Serial.print(values[i], 2);
|
||||||
|
if (channelUnits[i][0] != '\0') {
|
||||||
|
Serial.print(" ");
|
||||||
|
Serial.print(channelUnits[i]);
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
else if (connectionState == STATE_CONNECTED) {
|
||||||
|
// Normal UDP-driven update
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
if (!overrideActive[ch]) {
|
||||||
|
targetDuty[ch] = values[ch];
|
||||||
|
slewStartDuty[ch] = currentDuty[ch];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slewStartTime = millis();
|
||||||
|
lastPacketTime = millis();
|
||||||
|
|
||||||
|
// Debug output
|
||||||
|
Serial.println("Received UDP packet:");
|
||||||
|
for (int i = 0; i < NUM_CHANNELS; i++) {
|
||||||
|
Serial.print(" CH");
|
||||||
|
Serial.print(i);
|
||||||
|
Serial.print(" (");
|
||||||
|
Serial.print(channelLabels[i]);
|
||||||
|
Serial.print("): ");
|
||||||
|
Serial.print(values[i], 2);
|
||||||
|
if (channelUnits[i][0] != '\0') {
|
||||||
|
Serial.print(" ");
|
||||||
|
Serial.print(channelUnits[i]);
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Connection state machine
|
||||||
|
// -------------------------------
|
||||||
|
void coreUpdateState() {
|
||||||
|
unsigned long now = millis();
|
||||||
|
|
||||||
|
switch (connectionState) {
|
||||||
|
|
||||||
|
case STATE_CONNECTED: {
|
||||||
|
// Check for lost connection
|
||||||
|
if (now - lastPacketTime > watchdogTimeout) {
|
||||||
|
Serial.println("STATE CHANGE: CONNECTED → DISCONNECTED (UDP connection lost)");
|
||||||
|
connectionState = STATE_DISCONNECTED;
|
||||||
|
updateConnectionStatusUI(connectionState);
|
||||||
|
startLightingFade(lightingBrightness, 0, LIGHTING_FADE_OUT_DURATION);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal slew-rate limiting
|
||||||
|
float progress = (float)(now - slewStartTime) / (float)slewDuration;
|
||||||
|
if (progress > 1.0f) progress = 1.0f;
|
||||||
|
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
if (!overrideActive[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case STATE_CONNECTING: {
|
||||||
|
// If we lose packets even while connecting, fall back to DISCONNECTED
|
||||||
|
if (now - lastPacketTime > watchdogTimeout) {
|
||||||
|
Serial.println("STATE CHANGE: CONNECTING → DISCONNECTED (no packets during fade-in)");
|
||||||
|
connectionState = STATE_DISCONNECTED;
|
||||||
|
updateConnectionStatusUI(connectionState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fade-in animation: 0 → 100% on all non-override channels
|
||||||
|
if (now - lastFadeTime >= FADE_INTERVAL) {
|
||||||
|
lastFadeTime = now;
|
||||||
|
|
||||||
|
bool allReached = true;
|
||||||
|
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
if (!overrideActive[ch]) {
|
||||||
|
float cd = currentDuty[ch];
|
||||||
|
|
||||||
|
// Exponential approach to 100%
|
||||||
|
cd = cd * FADE_IN_FACTOR + 100.0f * (1.0f - FADE_IN_FACTOR);
|
||||||
|
currentDuty[ch] = cd;
|
||||||
|
|
||||||
|
float calibratedDuty = applyCalibration(ch, cd);
|
||||||
|
int duty = (int)((calibratedDuty / 100.0f) * ((1 << pwmResolution) - 1));
|
||||||
|
ledcWrite(pwmPins[ch], duty);
|
||||||
|
|
||||||
|
// Check if we're close enough to 100%
|
||||||
|
if (cd < 99.0f) {
|
||||||
|
allReached = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allReached) {
|
||||||
|
Serial.println("STATE CHANGE: CONNECTING → STATE_WAIT_FOR_FIRST_PACKET (fade-in complete)");
|
||||||
|
connectionState = STATE_WAIT_FOR_FIRST_PACKET;
|
||||||
|
updateConnectionStatusUI(connectionState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case STATE_WAIT_FOR_FIRST_PACKET: {
|
||||||
|
// Hold at ~100%, do nothing until first UDP packet arrives
|
||||||
|
// (handled in coreHandleUDP)
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case STATE_DISCONNECTED: {
|
||||||
|
// Watchdog fade-to-zero (always active in this state)
|
||||||
|
if (now - lastFadeTime >= FADE_INTERVAL) {
|
||||||
|
lastFadeTime = now;
|
||||||
|
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
|
||||||
|
if (currentDuty[ch] > 0.0f) {
|
||||||
|
|
||||||
|
currentDuty[ch] *= FADE_OUT_FACTOR;
|
||||||
|
|
||||||
|
if (currentDuty[ch] < 0.01f)
|
||||||
|
currentDuty[ch] = 0.0f;
|
||||||
|
|
||||||
|
float calibratedDuty = applyCalibration(ch, currentDuty[ch]);
|
||||||
|
int duty = (int)((calibratedDuty / 100.0f) * ((1 << pwmResolution) - 1));
|
||||||
|
|
||||||
|
ledcWrite(pwmPins[ch], duty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightingFading) {
|
||||||
|
unsigned long now = millis();
|
||||||
|
unsigned long elapsed = now - lightingFadeStartTime;
|
||||||
|
|
||||||
|
if (elapsed >= lightingFadeDuration) {
|
||||||
|
// Fade complete
|
||||||
|
lightingBrightness = lightingFadeEnd;
|
||||||
|
|
||||||
|
// If fade-out finished, jump to 0
|
||||||
|
if (lightingFadeEnd == 0) {
|
||||||
|
lightingBrightness = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lightingFading = false;
|
||||||
|
applyLighting();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute normalized progress
|
||||||
|
float t = (float)elapsed / (float)lightingFadeDuration;
|
||||||
|
|
||||||
|
// Gamma correction
|
||||||
|
const float gamma = 2.2f;
|
||||||
|
float t_gamma = pow(t, gamma);
|
||||||
|
|
||||||
|
// Determine effective fade range
|
||||||
|
uint8_t start = lightingFadeStart;
|
||||||
|
uint8_t end = lightingFadeEnd;
|
||||||
|
|
||||||
|
// Fade-in: 0 → 13 → 100
|
||||||
|
if (start == 0 && end > 0) {
|
||||||
|
start = BRIGHTNESS_MIN_VISIBLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fade-out: 100 → 13 → 0
|
||||||
|
if (end == 0 && start > 0) {
|
||||||
|
end = BRIGHTNESS_MIN_VISIBLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate only within the visible range
|
||||||
|
float raw = start + (end - start) * t_gamma;
|
||||||
|
|
||||||
|
// Apply jump logic:
|
||||||
|
if (lightingFadeStart == 0 && elapsed == 0) {
|
||||||
|
// Fade-in: first frame → jump to 13
|
||||||
|
lightingBrightness = BRIGHTNESS_MIN_VISIBLE;
|
||||||
|
}
|
||||||
|
else if (lightingFadeEnd == 0 && elapsed + 16 >= lightingFadeDuration) {
|
||||||
|
// Fade-out: last frame → jump to 0
|
||||||
|
lightingBrightness = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lightingBrightness = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyLighting();
|
||||||
|
}
|
||||||
|
}
|
||||||
58
analog_system_monitor_arduino/Core.h
Normal file
58
analog_system_monitor_arduino/Core.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <ESPUI.h>
|
||||||
|
#include "Config.h"
|
||||||
|
|
||||||
|
// Global objects
|
||||||
|
extern WiFiUDP udp;
|
||||||
|
extern Preferences prefs;
|
||||||
|
extern int udpPort;
|
||||||
|
|
||||||
|
// Duty tracking + slew
|
||||||
|
extern float currentDuty[NUM_CHANNELS];
|
||||||
|
extern float targetDuty[NUM_CHANNELS];
|
||||||
|
extern float slewStartDuty[NUM_CHANNELS];
|
||||||
|
|
||||||
|
extern unsigned long slewStartTime;
|
||||||
|
extern unsigned long lastPacketTime;
|
||||||
|
extern unsigned long lastFadeTime;
|
||||||
|
|
||||||
|
// Calibration
|
||||||
|
extern float logicalPoints[5];
|
||||||
|
extern float calibratedPoints[NUM_CHANNELS][5];
|
||||||
|
extern bool overrideActive[NUM_CHANNELS];
|
||||||
|
|
||||||
|
// Lighting parameters (0–255 range for now)
|
||||||
|
extern uint8_t lightingHue; // 0–255
|
||||||
|
extern uint8_t lightingSaturation; // 0–255
|
||||||
|
extern uint8_t lightingBrightness; // 0–255
|
||||||
|
extern uint8_t lightingBrightnessSaved;
|
||||||
|
|
||||||
|
// State
|
||||||
|
extern ConnectionState connectionState;
|
||||||
|
|
||||||
|
// UI IDs used by Core
|
||||||
|
extern uint16_t connectionStatusLabel;
|
||||||
|
extern uint16_t calChannelDropdown;
|
||||||
|
extern uint16_t calInputs[5];
|
||||||
|
extern uint16_t calTestValueInput;
|
||||||
|
extern uint16_t calSaveButton;
|
||||||
|
extern uint16_t calOverrideSwitch;
|
||||||
|
extern uint8_t selectedCalChannel;
|
||||||
|
|
||||||
|
// Core API
|
||||||
|
void coreInit(); // called from setup()
|
||||||
|
void coreHandleUDP(); // called from loop()
|
||||||
|
void coreUpdateState(); // called from loop()
|
||||||
|
|
||||||
|
// Helpers used by UI
|
||||||
|
float applyCalibration(uint8_t ch, float logicalDuty);
|
||||||
|
void updateConnectionStatusUI(ConnectionState state);
|
||||||
|
|
||||||
|
// Lighting helpers
|
||||||
|
void applyLighting();
|
||||||
|
void startLightingFade(uint8_t from, uint8_t to);
|
||||||
|
|
||||||
|
// Lighting fade state
|
||||||
|
extern bool lightingFading;
|
||||||
5
analog_system_monitor_arduino/UI.h
Normal file
5
analog_system_monitor_arduino/UI.h
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ESPUI.h>
|
||||||
|
#include "Core.h"
|
||||||
|
|
||||||
|
void uiInit(uint16_t& tabSettings, uint16_t& tabLighting, uint16_t& tabCalibration);
|
||||||
389
analog_system_monitor_arduino/Ui.cpp
Normal file
389
analog_system_monitor_arduino/Ui.cpp
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "UI.h"
|
||||||
|
|
||||||
|
// Local UI control
|
||||||
|
static uint16_t portInput;
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Calibration UI helpers & callbacks
|
||||||
|
// -------------------------------
|
||||||
|
void refreshCalibrationUI() {
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
ESPUI.updateControlValue(calInputs[i], String(calibratedPoints[selectedCalChannel][i], 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void calChannelCallback(Control *sender, int type) {
|
||||||
|
// Turn override OFF when switching channels
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
overrideActive[ch] = false;
|
||||||
|
}
|
||||||
|
ESPUI.updateControlValue(calOverrideSwitch, "0");
|
||||||
|
|
||||||
|
selectedCalChannel = sender->value.toInt();
|
||||||
|
Serial.print("Calibration channel changed to ");
|
||||||
|
Serial.println(selectedCalChannel);
|
||||||
|
|
||||||
|
refreshCalibrationUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
void calPointCallback(Control *sender, int type) {
|
||||||
|
int index = sender->id - calInputs[0];
|
||||||
|
if (index >= 0 && index < 5) {
|
||||||
|
float val = sender->value.toFloat();
|
||||||
|
calibratedPoints[selectedCalChannel][index] = val;
|
||||||
|
Serial.printf("Cal[%d][%d] = %.2f\n", selectedCalChannel, index, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void calTestCallback(Control *sender, int type) {
|
||||||
|
if (!overrideActive[selectedCalChannel]) return;
|
||||||
|
|
||||||
|
float logical = sender->value.toFloat(); // 0–100 integer
|
||||||
|
|
||||||
|
if (logical < 0) logical = 0;
|
||||||
|
if (logical > 100) logical = 100;
|
||||||
|
|
||||||
|
float calibrated = applyCalibration(selectedCalChannel, logical);
|
||||||
|
int duty = (int)((calibrated / 100.0f) * ((1 << pwmResolution) - 1));
|
||||||
|
|
||||||
|
ledcWrite(pwmPins[selectedCalChannel], duty);
|
||||||
|
|
||||||
|
Serial.printf("Override update CH%d: logical=%.2f calibrated=%.2f duty=%d\n",
|
||||||
|
selectedCalChannel, logical, calibrated, duty);
|
||||||
|
}
|
||||||
|
|
||||||
|
void calSaveCallback(Control *sender, int type) {
|
||||||
|
Serial.printf("Saving calibration for CH%d...\n", selectedCalChannel);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
String key = "cal_" + String(selectedCalChannel) + "_" + String(i);
|
||||||
|
prefs.putFloat(key.c_str(), calibratedPoints[selectedCalChannel][i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Calibration saved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void calOverrideSwitchCallback(Control *sender, int type) {
|
||||||
|
bool enabled = sender->value.toInt() == 1;
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
Serial.println("Override enabled.");
|
||||||
|
|
||||||
|
// Enable override only for the selected channel
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
overrideActive[ch] = (ch == selectedCalChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately apply the test value
|
||||||
|
float logical = ESPUI.getControl(calTestValueInput)->value.toFloat();
|
||||||
|
if (logical < 0) logical = 0;
|
||||||
|
if (logical > 100) logical = 100;
|
||||||
|
|
||||||
|
float calibrated = applyCalibration(selectedCalChannel, logical);
|
||||||
|
int duty = (int)((calibrated / 100.0f) * ((1 << pwmResolution) - 1));
|
||||||
|
ledcWrite(pwmPins[selectedCalChannel], duty);
|
||||||
|
|
||||||
|
Serial.printf("Override driving CH%d: logical=%.2f calibrated=%.2f duty=%d\n",
|
||||||
|
selectedCalChannel, logical, calibrated, duty);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Serial.println("Override disabled. Returning to UDP control.");
|
||||||
|
|
||||||
|
// Disable all overrides
|
||||||
|
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
||||||
|
overrideActive[ch] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void lightingSaveCallback(Control *sender, int type) {
|
||||||
|
if (type != B_UP) return; // avoid double-trigger
|
||||||
|
|
||||||
|
prefs.putUChar("light_hue", lightingHue);
|
||||||
|
prefs.putUChar("light_sat", lightingSaturation);
|
||||||
|
prefs.putUChar("light_bright", lightingBrightnessSaved);
|
||||||
|
|
||||||
|
Serial.printf("Lighting saved (0–255): H=%d S=%d B=%d\n",
|
||||||
|
lightingHue, lightingSaturation, lightingBrightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int toSlider(uint8_t v) {
|
||||||
|
return (int)((v / 255.0f) * 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t fromSlider(int v) {
|
||||||
|
return (uint8_t)((v / 100.0f) * 255.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// UI init
|
||||||
|
// -------------------------------
|
||||||
|
void uiInit(uint16_t& tabSettings, uint16_t& tabLighting, uint16_t& tabCalibration) {
|
||||||
|
|
||||||
|
// Create tabs
|
||||||
|
tabSettings = ESPUI.addControl(ControlType::Tab, "Settings", "Settings");
|
||||||
|
tabLighting = ESPUI.addControl(ControlType::Tab, "Lighting", "Lighting");
|
||||||
|
tabCalibration= ESPUI.addControl(ControlType::Tab, "Calibration", "Calibration");
|
||||||
|
|
||||||
|
// Restart button callback
|
||||||
|
auto restartCallback = [](Control *sender, int type) {
|
||||||
|
ESP.restart();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Port input callback
|
||||||
|
auto portInputCallback = [](Control *sender, int type) {
|
||||||
|
Serial.print("Port input changed to: ");
|
||||||
|
Serial.println(sender->value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save & Apply callback
|
||||||
|
auto savePortCallback = [](Control *sender, int type) {
|
||||||
|
if (type != B_UP) return; // Prevent double-trigger
|
||||||
|
|
||||||
|
Control* c = ESPUI.getControl(portInput);
|
||||||
|
int newPort = c->value.toInt();
|
||||||
|
|
||||||
|
if (newPort < 1024 || newPort > 65535) {
|
||||||
|
Serial.println("Invalid port (1024–65535)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.putInt("udpPort", newPort);
|
||||||
|
udpPort = newPort;
|
||||||
|
|
||||||
|
udp.stop();
|
||||||
|
udp.begin(udpPort);
|
||||||
|
|
||||||
|
Serial.print("New UDP port applied: ");
|
||||||
|
Serial.println(udpPort);
|
||||||
|
|
||||||
|
ESPUI.updateControlValue(portInput, String(newPort));
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// Connection Status section
|
||||||
|
// -------------------------------------------
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Separator,
|
||||||
|
"Connection Status",
|
||||||
|
"",
|
||||||
|
ControlColor::None,
|
||||||
|
tabSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
// Live-updating connection status label
|
||||||
|
connectionStatusLabel = ESPUI.addControl(
|
||||||
|
ControlType::Label,
|
||||||
|
"Status",
|
||||||
|
"Disconnected",
|
||||||
|
ControlColor::Wetasphalt,
|
||||||
|
tabSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// UDP Telemetry Connection Settings section
|
||||||
|
// -------------------------------------------
|
||||||
|
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Separator,
|
||||||
|
"UDP Telemetry Connection Settings",
|
||||||
|
"",
|
||||||
|
ControlColor::None,
|
||||||
|
tabSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
// UDP Port Number input
|
||||||
|
portInput = ESPUI.addControl(
|
||||||
|
ControlType::Number,
|
||||||
|
"UDP Port",
|
||||||
|
String(udpPort),
|
||||||
|
ControlColor::Peterriver,
|
||||||
|
tabSettings,
|
||||||
|
portInputCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save & Apply button
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Button,
|
||||||
|
"UDP Port",
|
||||||
|
"Save & Apply",
|
||||||
|
ControlColor::Emerald,
|
||||||
|
tabSettings,
|
||||||
|
savePortCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
// Existing separator (leave as-is)
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Separator,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
ControlColor::None,
|
||||||
|
tabSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restart button (unchanged)
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Button,
|
||||||
|
"Restart ESP32",
|
||||||
|
"Restart",
|
||||||
|
ControlColor::Alizarin,
|
||||||
|
tabSettings,
|
||||||
|
restartCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Lighting Controls
|
||||||
|
// -------------------------------
|
||||||
|
|
||||||
|
// Hue slider (0–360)
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Slider,
|
||||||
|
"Hue",
|
||||||
|
String(toSlider(lightingHue)),
|
||||||
|
ControlColor::Sunflower,
|
||||||
|
tabLighting,
|
||||||
|
[](Control *sender, int type) {
|
||||||
|
int sliderVal = sender->value.toInt(); // 0–100
|
||||||
|
lightingHue = fromSlider(sliderVal); // convert to 0–255
|
||||||
|
Serial.printf("Lighting Hue changed (RAM only): %d\n", lightingHue);
|
||||||
|
lightingFading = false; // cancel fade if user moves slider
|
||||||
|
applyLighting();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Saturation slider (0–100)
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Slider,
|
||||||
|
"Saturation",
|
||||||
|
String(toSlider(lightingSaturation)),
|
||||||
|
ControlColor::Carrot,
|
||||||
|
tabLighting,
|
||||||
|
[](Control *sender, int type) {
|
||||||
|
int sliderVal = sender->value.toInt(); // 0–100
|
||||||
|
lightingSaturation = fromSlider(sliderVal);
|
||||||
|
Serial.printf("Lighting Saturation updated (RAM only): %d\n", lightingSaturation);
|
||||||
|
lightingFading = false; // cancel fade if user moves slider
|
||||||
|
applyLighting();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Brightness slider (0–100)
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Slider,
|
||||||
|
"Brightness",
|
||||||
|
String(toSlider(lightingBrightness)),
|
||||||
|
ControlColor::Emerald,
|
||||||
|
tabLighting,
|
||||||
|
[](Control *sender, int type) {
|
||||||
|
int sliderVal = sender->value.toInt(); // 0–100
|
||||||
|
lightingBrightnessSaved = fromSlider(sliderVal);
|
||||||
|
lightingBrightness = lightingBrightnessSaved;
|
||||||
|
Serial.printf("Lighting Brightness updated (RAM only): %d\n", lightingBrightness);
|
||||||
|
lightingFading = false; // cancel fade if user moves slider
|
||||||
|
applyLighting();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Button,
|
||||||
|
"Save Lighting Settings",
|
||||||
|
"Save",
|
||||||
|
ControlColor::Emerald,
|
||||||
|
tabLighting,
|
||||||
|
lightingSaveCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Calibration tab UI
|
||||||
|
// -------------------------------
|
||||||
|
|
||||||
|
// Channel selector
|
||||||
|
calChannelDropdown = ESPUI.addControl(
|
||||||
|
ControlType::Select,
|
||||||
|
"Selected Channel",
|
||||||
|
"0",
|
||||||
|
ControlColor::Peterriver,
|
||||||
|
tabCalibration,
|
||||||
|
calChannelCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add options 0–7
|
||||||
|
for (int i = 0; i < NUM_CHANNELS; i++) {
|
||||||
|
char value[8];
|
||||||
|
snprintf(value, sizeof(value), "%d", i);
|
||||||
|
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Option,
|
||||||
|
channelDropdownLabels[i], // static label
|
||||||
|
value, // static-ish value (OK)
|
||||||
|
ControlColor::None,
|
||||||
|
calChannelDropdown
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Separator,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
ControlColor::None,
|
||||||
|
tabCalibration
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calibration inputs
|
||||||
|
const char* calNames[5] = {"0%", "25%", "50%", "75%", "100%"};
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
calInputs[i] = ESPUI.addControl(
|
||||||
|
ControlType::Number,
|
||||||
|
calNames[i],
|
||||||
|
String(calibratedPoints[0][i], 2),
|
||||||
|
ControlColor::Wetasphalt,
|
||||||
|
tabCalibration,
|
||||||
|
calPointCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Separator,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
ControlColor::None,
|
||||||
|
tabCalibration
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test value input
|
||||||
|
calTestValueInput = ESPUI.addControl(
|
||||||
|
ControlType::Slider,
|
||||||
|
"Test Value",
|
||||||
|
"50",
|
||||||
|
ControlColor::Carrot,
|
||||||
|
tabCalibration,
|
||||||
|
calTestCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
calOverrideSwitch = ESPUI.addControl(
|
||||||
|
ControlType::Switcher,
|
||||||
|
"Override",
|
||||||
|
"0", // default OFF
|
||||||
|
ControlColor::Alizarin,
|
||||||
|
tabCalibration,
|
||||||
|
calOverrideSwitchCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
ESPUI.addControl(
|
||||||
|
ControlType::Separator,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
ControlColor::None,
|
||||||
|
tabCalibration
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save button
|
||||||
|
calSaveButton = ESPUI.addControl(
|
||||||
|
ControlType::Button,
|
||||||
|
"Save Calibration",
|
||||||
|
"Save",
|
||||||
|
ControlColor::Emerald,
|
||||||
|
tabCalibration,
|
||||||
|
calSaveCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,146 +1,66 @@
|
|||||||
|
//
|
||||||
|
// IMPORTANT:
|
||||||
|
// before flashing this the first time,
|
||||||
|
// upload the "prepareFilesystem.ino" example sketch from ESPUI
|
||||||
|
// without that critical files are missing
|
||||||
|
//
|
||||||
|
// This was done to reduce the PROGMEM footprint of this program
|
||||||
|
//
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <RP2040_PWM.h>
|
#include <WiFi.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
|
||||||
// -------------------------------
|
#include <AsyncTCP.h>
|
||||||
// Firmware version
|
#include <ESPAsyncWebServer.h>
|
||||||
// -------------------------------
|
#include <ESPUI.h>
|
||||||
const char* FIRMWARE_VERSION = "V1.0";
|
|
||||||
|
|
||||||
// -------------------------------
|
#include "Config.h"
|
||||||
// PWM setup
|
#include "Core.h"
|
||||||
// -------------------------------
|
#include "UI.h"
|
||||||
|
|
||||||
const uint8_t NUM_CHANNELS = 8;
|
AsyncWebServer server(80);
|
||||||
uint8_t pwmPins[NUM_CHANNELS] = {14, 15, 26, 27, 8, 7, 6, 5};
|
|
||||||
|
|
||||||
const uint32_t pwmFrequency = 10000;
|
|
||||||
|
|
||||||
// -------------------------------
|
|
||||||
// Calibration tables
|
|
||||||
// -------------------------------
|
|
||||||
|
|
||||||
float logicalPoints[5] = {0, 25, 50, 75, 100};
|
|
||||||
|
|
||||||
float calibratedPoints[NUM_CHANNELS][5] = {
|
|
||||||
{0, 25, 50, 75, 99},
|
|
||||||
{0, 24, 49, 74, 98},
|
|
||||||
{0, 26, 51, 76, 99},
|
|
||||||
{0, 25, 50, 75, 97},
|
|
||||||
{0, 25, 50, 75, 99},
|
|
||||||
{0, 24, 50, 74, 98},
|
|
||||||
{0, 25, 49, 75, 97},
|
|
||||||
{0, 26, 50, 76, 99}
|
|
||||||
};
|
|
||||||
|
|
||||||
// -------------------------------
|
|
||||||
// Duty tracking
|
|
||||||
// -------------------------------
|
|
||||||
|
|
||||||
float currentDuty[NUM_CHANNELS] = {0.0f};
|
|
||||||
RP2040_PWM* pwm[NUM_CHANNELS];
|
|
||||||
|
|
||||||
// -------------------------------
|
|
||||||
// Watchdog
|
|
||||||
// -------------------------------
|
|
||||||
|
|
||||||
unsigned long lastSerialTime = 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];
|
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
if (logicalDuty >= logicalPoints[i] && logicalDuty <= logicalPoints[i+1]) {
|
|
||||||
float x0 = logicalPoints[i];
|
|
||||||
float x1 = logicalPoints[i+1];
|
|
||||||
float y0 = calibratedPoints[ch][i];
|
|
||||||
float y1 = calibratedPoints[ch][i+1];
|
|
||||||
float t = (logicalDuty - x0) / (x1 - x0);
|
|
||||||
return y0 + t * (y1 - y0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return calibratedPoints[ch][4];
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------
|
|
||||||
// Setup
|
|
||||||
// -------------------------------
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(300);
|
delay(300);
|
||||||
|
|
||||||
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
Serial.println("Booting Analog System Monitor (UDP + LEDC + WiFiManager + Slew)");
|
||||||
pwm[ch] = new RP2040_PWM(pwmPins[ch], pwmFrequency, 0.0f);
|
Serial.print("Firmware: ");
|
||||||
if (pwm[ch]) pwm[ch]->setPWM();
|
Serial.println(FIRMWARE_VERSION);
|
||||||
|
|
||||||
|
// WiFi Manager
|
||||||
|
WiFiManager wm;
|
||||||
|
wm.setHostname("AnalogMonitor");
|
||||||
|
wm.setTimeout(180);
|
||||||
|
|
||||||
|
Serial.println("Starting WiFiManager...");
|
||||||
|
bool res = wm.autoConnect("AnalogMonitor-Setup");
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
Serial.println("WiFi failed or timed out. Rebooting...");
|
||||||
|
delay(2000);
|
||||||
|
ESP.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSerialTime = millis();
|
Serial.println("WiFi connected!");
|
||||||
}
|
Serial.print("IP: ");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
|
||||||
// -------------------------------
|
// Core init (PWM, prefs, UDP, calibration, state)
|
||||||
// Loop
|
coreInit();
|
||||||
// -------------------------------
|
|
||||||
|
// ESPUI Web Interface
|
||||||
|
ESPUI.setVerbosity(Verbosity::Verbose);
|
||||||
|
|
||||||
|
uint16_t tabSettings, tabLighting, tabCalibration;
|
||||||
|
uiInit(tabSettings, tabLighting, tabCalibration);
|
||||||
|
|
||||||
|
ESPUI.sliderContinuous = true; // enables live slider updates
|
||||||
|
ESPUI.beginLITTLEFS("Analog System Monitor UI");
|
||||||
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
coreHandleUDP();
|
||||||
// -------- Serial parsing --------
|
coreUpdateState();
|
||||||
while (Serial.available()) {
|
|
||||||
String s = Serial.readStringUntil('\n');
|
|
||||||
s.trim();
|
|
||||||
|
|
||||||
// --- Device identification command ---
|
|
||||||
if (s.equalsIgnoreCase("PING")) {
|
|
||||||
Serial.print("Analog_System_Monitor_");
|
|
||||||
Serial.println(FIRMWARE_VERSION);
|
|
||||||
lastSerialTime = millis();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int eq = s.indexOf('=');
|
|
||||||
if (eq > 0) {
|
|
||||||
int ch = s.substring(0, eq).toInt();
|
|
||||||
float val = s.substring(eq + 1).toFloat();
|
|
||||||
if (ch >= 0 && ch < NUM_CHANNELS && val >= 0.0f && val <= 100.0f) {
|
|
||||||
|
|
||||||
currentDuty[ch] = val;
|
|
||||||
|
|
||||||
float calibratedDuty = applyCalibration(ch, currentDuty[ch]);
|
|
||||||
pwm[ch]->setPWM(pwmPins[ch], pwmFrequency, calibratedDuty);
|
|
||||||
|
|
||||||
lastSerialTime = millis(); // reset watchdog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- Watchdog fade-to-zero (time-based exponential) --------
|
|
||||||
if (millis() - lastSerialTime > watchdogTimeout) {
|
|
||||||
|
|
||||||
const unsigned long fadeInterval = 1;
|
|
||||||
|
|
||||||
if (millis() - lastFadeTime >= fadeInterval) {
|
|
||||||
lastFadeTime = millis();
|
|
||||||
|
|
||||||
for (int ch = 0; ch < NUM_CHANNELS; ch++) {
|
|
||||||
|
|
||||||
if (currentDuty[ch] > 0.0f) {
|
|
||||||
|
|
||||||
const float fadeFactor = 0.999f;
|
|
||||||
currentDuty[ch] *= fadeFactor;
|
|
||||||
|
|
||||||
if (currentDuty[ch] < 0.01f)
|
|
||||||
currentDuty[ch] = 0.0f;
|
|
||||||
|
|
||||||
float calibratedDuty = applyCalibration(ch, currentDuty[ch]);
|
|
||||||
pwm[ch]->setPWM(pwmPins[ch], pwmFrequency, calibratedDuty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
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]
|
[STAThread]
|
||||||
@@ -12,4 +10,3 @@ namespace analog_system_monitor
|
|||||||
Application.Run(new TrayApp());
|
Application.Run(new TrayApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,338 +1,278 @@
|
|||||||
|
#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 UdpSender udp = new UdpSender();
|
||||||
|
private Computer computer = new Computer();
|
||||||
|
|
||||||
|
private IHardware? cpuHw;
|
||||||
|
private IHardware? gpuHw;
|
||||||
|
private IHardware? memHw;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
// Restart LHM every 30 minutes
|
||||||
|
private readonly TimeSpan restartInterval = TimeSpan.FromMinutes(30);
|
||||||
|
private DateTime lastRestart = DateTime.UtcNow;
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
config = Config.Load();
|
ConfigureComputer();
|
||||||
|
computer.Open();
|
||||||
|
CacheHardwareAndSensors();
|
||||||
|
}
|
||||||
|
|
||||||
// Load defaults from config
|
private void ConfigureComputer()
|
||||||
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.IsCpuEnabled = true;
|
||||||
computer.IsGpuEnabled = true;
|
computer.IsGpuEnabled = true;
|
||||||
|
computer.IsMemoryEnabled = true;
|
||||||
|
|
||||||
|
// True minimal mode
|
||||||
|
computer.IsMotherboardEnabled = false;
|
||||||
|
computer.IsControllerEnabled = false;
|
||||||
|
computer.IsNetworkEnabled = false;
|
||||||
|
computer.IsStorageEnabled = false;
|
||||||
|
computer.IsBatteryEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestartComputerIfNeeded()
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow - lastRestart < restartInterval)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastRestart = DateTime.UtcNow;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
computer.Close();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
computer = new Computer();
|
||||||
|
ConfigureComputer();
|
||||||
computer.Open();
|
computer.Open();
|
||||||
|
CacheHardwareAndSensors();
|
||||||
DetectSensors();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int UpdateRateMs => config.UpdateRateMs;
|
private void CacheHardwareAndSensors()
|
||||||
|
|
||||||
private void ParseArgs(string[] args)
|
|
||||||
{
|
{
|
||||||
foreach (var arg in args)
|
cpuHw = null;
|
||||||
|
gpuHw = null;
|
||||||
|
memHw = null;
|
||||||
|
|
||||||
|
cpuLoadSensors = Array.Empty<ISensor>();
|
||||||
|
cpuTempSensor = null;
|
||||||
|
|
||||||
|
gpu3DLoadSensor = null;
|
||||||
|
gpuTempSensor = null;
|
||||||
|
gpuVramUsedSensor = null;
|
||||||
|
gpuVramTotalSensor = null;
|
||||||
|
|
||||||
|
memUsedSensor = null;
|
||||||
|
memAvailSensor = null;
|
||||||
|
|
||||||
|
foreach (var hw in computer.Hardware)
|
||||||
{
|
{
|
||||||
if (arg.StartsWith("--ip="))
|
hw.Update();
|
||||||
oscIp = arg.Substring("--ip=".Length);
|
|
||||||
|
|
||||||
if (arg.StartsWith("--port=") &&
|
switch (hw.HardwareType)
|
||||||
int.TryParse(arg.Substring("--port=".Length), out int p))
|
{
|
||||||
oscPort = p;
|
case HardwareType.Cpu:
|
||||||
|
cpuHw = hw;
|
||||||
|
CacheCpuSensors(hw);
|
||||||
|
break;
|
||||||
|
|
||||||
if (arg.StartsWith("--rate=") &&
|
case HardwareType.GpuNvidia:
|
||||||
int.TryParse(arg.Substring("--rate=".Length), out int r))
|
case HardwareType.GpuAmd:
|
||||||
config.UpdateRateMs = r;
|
case HardwareType.GpuIntel:
|
||||||
|
gpuHw = hw;
|
||||||
|
CacheGpuSensors(hw);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HardwareType.Memory:
|
||||||
|
memHw = hw;
|
||||||
|
CacheMemorySensors(hw);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CacheCpuSensors(IHardware hw)
|
||||||
|
{
|
||||||
|
var loads = new System.Collections.Generic.List<ISensor>();
|
||||||
|
|
||||||
// ---------------- MAIN UPDATE LOOP ----------------
|
foreach (var s in hw.Sensors)
|
||||||
|
{
|
||||||
|
if (s.SensorType == SensorType.Load &&
|
||||||
|
s.Name.Contains("CPU Core"))
|
||||||
|
loads.Add(s);
|
||||||
|
|
||||||
|
if (s.SensorType == SensorType.Temperature)
|
||||||
|
{
|
||||||
|
if (s.Name == "Core (Tctl/Tdie)")
|
||||||
|
cpuTempSensor = s;
|
||||||
|
else if (cpuTempSensor == null)
|
||||||
|
cpuTempSensor = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuLoadSensors = loads.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CacheGpuSensors(IHardware hw)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateAndSend()
|
public void UpdateAndSend()
|
||||||
{
|
{
|
||||||
int memPercent = GetMemoryUsagePercent();
|
RestartComputerIfNeeded();
|
||||||
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 DetectSensors()
|
|
||||||
{
|
|
||||||
var cpuLoadList = new List<ISensor>();
|
|
||||||
|
|
||||||
ISensor? bestCpuTemp = null;
|
|
||||||
ISensor? bestGpuTemp = null;
|
|
||||||
ISensor? bestGpu3D = null;
|
|
||||||
ISensor? bestVramUsed = null;
|
|
||||||
ISensor? bestVramTotal = null;
|
|
||||||
|
|
||||||
|
//cpuHw?.Update();
|
||||||
|
//gpuHw?.Update();
|
||||||
|
//memHw?.Update();
|
||||||
|
// Single-pass update: update ALL enabled hardware in one loop
|
||||||
foreach (var hw in computer.Hardware)
|
foreach (var hw in computer.Hardware)
|
||||||
{
|
|
||||||
hw.Update();
|
hw.Update();
|
||||||
|
|
||||||
if (hw.HardwareType == HardwareType.Cpu)
|
float cpu = GetCpuLoadPercent();
|
||||||
|
float cpuTemp = GetCpuTemperaturePercent();
|
||||||
|
float mem = GetMemoryUsagePercent();
|
||||||
|
float gpu3d = GetGpu3DLoad();
|
||||||
|
float gpuTemp = GetGpuTemperaturePercent();
|
||||||
|
float vram = GetGpuVramPercent();
|
||||||
|
|
||||||
|
float[] packet =
|
||||||
{
|
{
|
||||||
foreach (var sensor in hw.Sensors)
|
cpu,
|
||||||
{
|
cpuTemp,
|
||||||
if (sensor.SensorType == SensorType.Load &&
|
mem,
|
||||||
sensor.Name.Contains("CPU Core"))
|
gpu3d,
|
||||||
cpuLoadList.Add(sensor);
|
gpuTemp,
|
||||||
|
vram,
|
||||||
|
0f,
|
||||||
|
0f
|
||||||
|
};
|
||||||
|
|
||||||
if (sensor.SensorType == SensorType.Temperature)
|
udp.SendFloats(packet);
|
||||||
{
|
|
||||||
if (sensor.Name == "Core (Tctl/Tdie)")
|
|
||||||
bestCpuTemp = sensor;
|
|
||||||
else if (bestCpuTemp == null)
|
|
||||||
bestCpuTemp = sensor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hw.HardwareType == HardwareType.GpuNvidia ||
|
private float GetCpuLoadPercent()
|
||||||
hw.HardwareType == HardwareType.GpuAmd ||
|
|
||||||
hw.HardwareType == HardwareType.GpuIntel)
|
|
||||||
{
|
|
||||||
foreach (var sensor in hw.Sensors)
|
|
||||||
{
|
|
||||||
if (sensor.SensorType == SensorType.Temperature)
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
cpuTempSensor = bestCpuTemp;
|
|
||||||
gpuTempSensor = bestGpuTemp;
|
|
||||||
gpu3DLoadSensor = bestGpu3D;
|
|
||||||
gpuVramUsedSensor = bestVramUsed;
|
|
||||||
gpuVramTotalSensor = bestVramTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- METRICS ----------------
|
|
||||||
|
|
||||||
private int GetMemoryUsagePercent()
|
|
||||||
{
|
|
||||||
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[]>();
|
udp.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,148 @@
|
|||||||
using System;
|
#nullable enable
|
||||||
using System.Drawing;
|
|
||||||
using System.Reflection;
|
using System;
|
||||||
using System.Threading;
|
using System.Diagnostics;
|
||||||
using System.Windows.Forms;
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace analog_system_monitor
|
|
||||||
{
|
|
||||||
public class TrayApp : ApplicationContext
|
public class TrayApp : ApplicationContext
|
||||||
{
|
{
|
||||||
private NotifyIcon trayIcon;
|
private NotifyIcon trayIcon;
|
||||||
private Thread workerThread;
|
private Telemetry telemetry;
|
||||||
private bool running = true;
|
private System.Windows.Forms.Timer timer;
|
||||||
|
private bool telemetryPaused = false;
|
||||||
|
|
||||||
public TrayApp()
|
public TrayApp()
|
||||||
{
|
{
|
||||||
trayIcon = new NotifyIcon()
|
telemetry = new Telemetry();
|
||||||
{
|
|
||||||
Icon = LoadEmbeddedIcon(),
|
|
||||||
Text = "Analog System Monitor",
|
|
||||||
Visible = true,
|
|
||||||
ContextMenuStrip = BuildMenu()
|
|
||||||
};
|
|
||||||
|
|
||||||
workerThread = new Thread(WorkerLoop)
|
|
||||||
{
|
|
||||||
IsBackground = true
|
|
||||||
};
|
|
||||||
workerThread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Icon LoadEmbeddedIcon()
|
|
||||||
{
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
var resourceName = "analog_system_monitor.telemetry_icon.ico";
|
|
||||||
|
|
||||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
|
||||||
if (stream == null)
|
|
||||||
{
|
|
||||||
MessageBox.Show("Embedded icon not found: " + resourceName);
|
|
||||||
return SystemIcons.Application;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
telemetry.Initialize();
|
||||||
|
|
||||||
while (running)
|
trayIcon = new NotifyIcon()
|
||||||
{
|
{
|
||||||
|
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath),
|
||||||
|
Visible = true,
|
||||||
|
Text = "Telemetry Running (UDP)"
|
||||||
|
};
|
||||||
|
|
||||||
|
var menu = new ContextMenuStrip();
|
||||||
|
|
||||||
|
// Show config.json
|
||||||
|
menu.Items.Add("Show Config", null, OnShowConfig);
|
||||||
|
|
||||||
|
// Reload config
|
||||||
|
menu.Items.Add("Reload Config", null, OnReloadConfig);
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
menu.Items.Add(new ToolStripSeparator());
|
||||||
|
|
||||||
|
// Exit
|
||||||
|
menu.Items.Add("Exit", null, OnExit);
|
||||||
|
|
||||||
|
trayIcon.ContextMenuStrip = menu;
|
||||||
|
|
||||||
|
// Main telemetry timer
|
||||||
|
timer = new System.Windows.Forms.Timer();
|
||||||
|
timer.Interval = 1000;
|
||||||
|
timer.Tick += (s, e) =>
|
||||||
|
{
|
||||||
|
if (!telemetryPaused)
|
||||||
telemetry.UpdateAndSend();
|
telemetry.UpdateAndSend();
|
||||||
Thread.Sleep(telemetry.UpdateRateMs);
|
};
|
||||||
}
|
timer.Start();
|
||||||
|
|
||||||
|
// Detect system sleep/wake
|
||||||
|
SystemEvents.PowerModeChanged += OnPowerModeChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExitApp()
|
private void OnPowerModeChanged(object? sender, PowerModeChangedEventArgs e)
|
||||||
{
|
{
|
||||||
running = false;
|
if (e.Mode == PowerModes.Suspend)
|
||||||
|
{
|
||||||
|
telemetryPaused = true;
|
||||||
|
}
|
||||||
|
else if (e.Mode == PowerModes.Resume)
|
||||||
|
{
|
||||||
|
telemetryPaused = true;
|
||||||
|
|
||||||
|
// Give Windows time to restore networking
|
||||||
|
var resumeTimer = new System.Windows.Forms.Timer();
|
||||||
|
resumeTimer.Interval = 3000; // 3 seconds
|
||||||
|
resumeTimer.Tick += (s, ev) =>
|
||||||
|
{
|
||||||
|
telemetryPaused = false;
|
||||||
|
resumeTimer.Stop();
|
||||||
|
resumeTimer.Dispose();
|
||||||
|
};
|
||||||
|
resumeTimer.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show config.json in Explorer
|
||||||
|
private void OnShowConfig(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
workerThread?.Join(500);
|
string exeDir = AppContext.BaseDirectory;
|
||||||
|
string cfgPath = Path.Combine(exeDir, "config.json");
|
||||||
|
|
||||||
|
if (File.Exists(cfgPath))
|
||||||
|
{
|
||||||
|
Process.Start("explorer.exe", $"/select,\"{cfgPath}\"");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MessageBox.Show(
|
||||||
|
"config.json not found.",
|
||||||
|
"Show Config",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Warning
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(
|
||||||
|
$"Failed to open config.json:\n{ex.Message}",
|
||||||
|
"Show Config Error",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Error
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
|
|
||||||
|
// Reload config handler
|
||||||
|
private void OnReloadConfig(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
telemetry.Dispose();
|
||||||
|
telemetry = new Telemetry();
|
||||||
|
telemetry.Initialize();
|
||||||
|
|
||||||
|
MessageBox.Show(
|
||||||
|
"Configuration reloaded successfully.",
|
||||||
|
"Reload Config",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Information
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(
|
||||||
|
$"Failed to reload configuration:\n{ex.Message}",
|
||||||
|
"Reload Config Error",
|
||||||
|
MessageBoxButtons.OK,
|
||||||
|
MessageBoxIcon.Error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExit(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
telemetry.Dispose();
|
||||||
trayIcon.Visible = false;
|
trayIcon.Visible = false;
|
||||||
trayIcon.Dispose();
|
|
||||||
|
|
||||||
Application.Exit();
|
Application.Exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
85
analog_system_monitor_dotnet/UdpSender.cs
Normal file
85
analog_system_monitor_dotnet/UdpSender.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#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<UdpConfig>(jsonText)
|
||||||
|
?? throw new Exception("Invalid config.json");
|
||||||
|
|
||||||
|
endpoint = new IPEndPoint(IPAddress.Parse(cfg.esp32_ip), cfg.esp32_port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendFloats(float[] values)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string packet = string.Join(",", values);
|
||||||
|
byte[] data = System.Text.Encoding.ASCII.GetBytes(packet);
|
||||||
|
|
||||||
|
client.Send(data, data.Length, endpoint);
|
||||||
|
}
|
||||||
|
catch (SocketException ex) when (
|
||||||
|
ex.SocketErrorCode == SocketError.NetworkUnreachable ||
|
||||||
|
ex.SocketErrorCode == SocketError.HostUnreachable ||
|
||||||
|
ex.SocketErrorCode == SocketError.NetworkDown ||
|
||||||
|
ex.SocketErrorCode == SocketError.AddressNotAvailable)
|
||||||
|
{
|
||||||
|
// Network not ready (sleep, reconnecting, etc.)
|
||||||
|
// Skip this tick silently.
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// App is shutting down — ignore.
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Any other unexpected error — swallow to avoid crashing the tray app.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
client.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UdpConfig
|
||||||
|
{
|
||||||
|
public string esp32_ip { get; set; } = DefaultIp;
|
||||||
|
public int esp32_port { get; set; } = DefaultPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,23 @@
|
|||||||
<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) -->
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
|
||||||
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
|
||||||
|
<!-- Keep debugging symbols optional -->
|
||||||
|
<DebugType>none</DebugType>
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
|
|
||||||
<ApplicationIcon>telemetry_icon.ico</ApplicationIcon>
|
<ApplicationIcon>telemetry_icon.ico</ApplicationIcon>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
35
analog_system_monitor_dotnet/app.manifest
Normal file
35
analog_system_monitor_dotnet/app.manifest
Normal 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>
|
||||||
53
analog_system_monitor_dotnet/build-release.ps1
Normal file
53
analog_system_monitor_dotnet/build-release.ps1
Normal 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
|
||||||
@@ -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 "============================================"
|
|
||||||
Reference in New Issue
Block a user