repromod-tester/Repromod_Tester_v1.ino
2025-11-03 17:25:49 +01:00

2694 lines
62 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
===============================================================
REPROMOD TESTER v1.0 (2025-10-31 21:47)
© 2025 Repromod® Marca registrada
Licencia: Creative Commons BY-NC-SA 4.0 International
https://creativecommons.org/licenses/by-nc-sa/4.0/
Descripción:
Analizador/inyector basado en Arduino Mega + INA226 + OLED SSD1306.
Modos: HOME/Gráfica SAFE (canal 1), Voltímetro, Tester Chip (CPU/GPU/VRAM/BUCK),
y Portátil (1821V). SAFE protege por caída brusca de V; SAFE+ añade pico de corriente.
Dónde editar configuración en el código:
- Direcciones/Dimensiones OLED e I2C: buscar definiciones de OLED_* e INA_ADDR
- Calibraciones: SHUNT_OHMS, V_CAL, I_CAL
- Pines: RELAY1/RELAY2/BTN1/BTN2/BEEPER
Botonera:
- B1: Canal1 / acciones contextuales (armar, rearmar, medir)
- B2: Menú / salir / cambiar SAFE (según modo)
- B1+B2 (hold): Bloque de emergencia → ciclo relés y WDT reset
Bloque de emergencia:
Doble pulsación mantenida BTN1+BTN2 ejecuta la rutina de seguridad:
conmuta relés 3 veces y reinicia por watchdog (WDT).
===============================================================
*/
/* Notas para operador (resumen práctico)
- SAFE / SAFE+: en la gráfica de Canal 1, pulsa B2 para rotar: SAFE → NO SAFE → SAFE+.
SAFE corta si V < 0.30 V tras el arm (protege por corto). SAFE+ añade corte por pico de I.
- Voltímetro: B2 para salir (aviso pinzas). Estado: ESTABLE / FLUCTUANTE / PULSANTE.
- Tester Chip: B2 rota la categoría; B1 inicia la medición. Si el ajuste no es 1.0 V (1.2 V VRAM), avisa.
- Portátil: B1 valida 1821 V y entra en análisis. B1 conmuta relé; B2 vuelve atrás.
- OLED Sleep: solo en HOME+IDLE. Cualquier tecla despierta (se ignora el primer click tras despertar).
*/
#include <avr/wdt.h>
#include <Wire.h>
#include <Adafruit_GFX.h> //V.1.11.9
#include <Adafruit_SSD1306.h> //V.2.5.7
#include <INA226.h> // ROB TILLAART V.0.4.4
#include <math.h>
// ======================== CONFIGURACIÓN BÁSICA// =====================================================
// PANTALLA OLED *No altere la resolución
#define OLED_ADDR 0x3C
#define OLED_W 128
#define OLED_H 64
Adafruit_SSD1306 display(OLED_W, OLED_H, &Wire);
// INA226 *Cálculo de medición en referencia al valor de la fuente
#define INA_ADDR 0x40
INA226 ina(INA_ADDR);
// Calibración campo
const float SHUNT_OHMS = 0.005f; // 5 mΩ, si tienes otra resistencia cámbiala en tu placa a esta
// Si metiste divisor de tensión de 100k/100k a VBUS multiplicamos x2:
const float V_CAL = 1.0501f * 2.0f; // ajusta fino de voltaje
// Ajuste el fino de amperajes
const float I_CAL = 0.810f; // Este ajuste es para (2.47A → 2.00A) relación 1:!
// CONFIGURACIÓN DE PINES
#define RELAY1 7 // canal 1 con SAFE / Led indicador
#define RELAY2 6 // canal 2 libre / Led indicador
#define BTN1 9 // Botón 1
#define BTN2 10 // Botón 2
#define BEEPER 5 // Beeper
// =============================FIN DE CONFIGURACIÓN// ================================================
enum Ch1State {
CH1_IDLE = 0,
CH1_ACTIVE,
CH1_TRIPPED
};
Ch1State ch1State = CH1_IDLE;
bool wakeSwallowInputs = false;
bool safeModeEnabled = true;
float tc_lastAdjV = NAN;
float tc_lastAdjA = NAN;
const float SAFE_SHORT_VOLTAGE = 0.40f;
const unsigned long SAFE_ARM_DELAY_MS = 200;
enum SafeMode : uint8_t { SAFE_V = 0, SAFE_OFF = 1, SAFE_PLUS = 2 };
SafeMode safeMode = SAFE_V;
bool lastTripWasPlus = false;
unsigned long ch1OnMs = 0;
float tc_aPrev = NAN;
float tc_aP2P = 0.0f;
float tc_aStepMax = 0.0f;
uint16_t tc_runMs = 4000;
uint16_t tc_settleMs = 200;
bool lastShortTrip = false;
bool tripScreenDrawn = false;
enum TestChipSel_TC : uint8_t {
TCSEL_CPU = 0,
TCSEL_GPU = 1,
TCSEL_VRAM = 2,
TCSEL_BUCK = 3,
TCSEL_COUNT = 4
};
uint8_t testChipSel_TC = TCSEL_CPU;
enum TestChipPhase_TC : uint8_t {
TCPH_READY = 0,
TCPH_ARMED,
TCPH_MEASURE,
TCPH_RESULT
};
TestChipPhase_TC testPhase_TC = TCPH_READY;
enum LaptopPhase { LAPH_ADJ = 0, LAPH_ANALYSIS = 1 };
LaptopPhase laptopPhase = LAPH_ADJ;
enum TestChipResult_TC : uint8_t {
TCR_NONE = 0,
TCR_OK,
TCR_VLOW,
TCR_VHIGH,
TCR_SHORT,
TCR_FAIL,
TCR_NOCON
};
TestChipResult_TC lastResult_TC = TCR_NONE;
bool RELAY_ACTIVE_LOW = false;
inline void setRelay(uint8_t pin, bool on){
digitalWrite(pin,
RELAY_ACTIVE_LOW
? (on ? LOW : HIGH)
: (on ? HIGH : LOW)
);
}
bool relay1=false;
bool relay2=false;
const char *FW_VERSION = "Tester v1.0";
const uint16_t TC_TRACE_MAX = 120;
float tc_trace[TC_TRACE_MAX];
uint16_t tc_traceCount = 0;
uint8_t tc_finalCode = 4;
float tc_vMin = 99.0f;
float tc_vMax = 0.0f;
float tc_vSum = 0.0f;
uint16_t tc_cnt = 0;
float tc_lastCheckVolt = 0.0f;
float tc_aMin = 99.0f;
float tc_aMax = 0.0f;
float tc_aSum = 0.0f;
float tc_vMinStable = 99.0f;
float tc_vMaxStable = 0.0f;
float tc_aSumStable = 0.0f;
uint16_t tc_cntStable = 0;
float tc_aMinStable = 99.0f;
float tc_aMaxStable = 0.0f;
unsigned long tc_stableStartMs = 0;
float tc_aSumEarly = 0.0f; uint16_t tc_aCntEarly = 0;
float tc_aSumLate = 0.0f; uint16_t tc_aCntLate = 0;
unsigned long tcMeasureStartMs = 0;
unsigned long TC_MEASURE_WINDOW_MS = 2000;
const float TC_OK_MIN_V = 0.90f;
const float TC_OK_MAX_V = 1.10f;
const float TC_SHORT_V = 0.30f;
const float TC_HIGH_V = 1.40f;
bool laptopNeedsAdj = true;
const float LAPTOP_VMIN = 18.0f;
const float LAPTOP_VMAX = 21.0f;
const float L_NO_POWER_A = 0.010f;
const float L_STBY_MIN_A = 0.015f;
const float L_STBY_MAX_A = 0.050f;
const float L_COMM_SWING_A = 0.020f;
const float L_BOOT_A = 0.50f;
const float L_SHORT_I = 3.0f;
const float L_SHORT_DROP_P = 0.10f;
static float lap_aFast = 0.0f, lap_aSlow = 0.0f, lap_aSwingPeak = 0.0f;
static float lap_vBase = 0.0f;
static bool lap_lastRelayOn = false;
static bool lap_wasAboveStandby = false;
static unsigned long lap_lastAboveTs = 0;
static unsigned long lap_lastSampleMs = 0;
#include <avr/wdt.h>
void delaySafe(unsigned long ms){
unsigned long t0 = millis();
while (millis() - t0 < ms){
wdt_reset();
delay(10);
}
}
void repromod_dualButtonResetHandler(){
static uint8_t state = 0;
static unsigned long t0 = 0;
const bool b1 = (digitalRead(BTN1) == LOW);
const bool b2 = (digitalRead(BTN2) == LOW);
switch(state){
case 0:
if (b1 && b2){
t0 = millis();
state = 1;
}
break;
case 1:
if (!(b1 && b2)){
state = 0;
break;
}
if (millis() - t0 >= 250){
for (int i = 0; i < 3; i++){
setRelay(RELAY1, true);
setRelay(RELAY2, true);
delay(120);
setRelay(RELAY1, false);
setRelay(RELAY2, false);
delay(200);
}
setRelay(RELAY1, false);
setRelay(RELAY2, false);
wdt_enable(WDTO_15MS);
while (true) { }
}
break;
}
}
void beepOnce(uint16_t ms){
tone(BEEPER, 3000);
delay(ms);
noTone(BEEPER);
}
void beepCount(uint8_t n, uint16_t onMs=80, uint16_t offMs=80){
for(uint8_t i=0;i<n;i++){
beepOnce(onMs);
if(i+1<n) delay(offMs);
}
}
void beepLongWarning(){
tone(BEEPER, 2500);
delay(3000);
noTone(BEEPER);
}
bool lastBtn1=HIGH, lastBtn2=HIGH;
unsigned long lastDeb1=0,lastDeb2=0;
const unsigned long DEBOUNCE_MS=50;
bool b2Held=false;
bool b2EscapeDone=false;
unsigned long b2PressStart=0;
const unsigned long B2_LONG_MS = 600;
bool consumeClick(uint8_t pin){
if (wakeSwallowInputs){
if (digitalRead(BTN1)==HIGH && digitalRead(BTN2)==HIGH){
wakeSwallowInputs = false;
lastBtn1 = HIGH; lastBtn2 = HIGH;
}
return false;
}
bool *lastPtr=(pin==BTN1)? &lastBtn1 : &lastBtn2;
unsigned long *debPtr=(pin==BTN1)? &lastDeb1 : &lastDeb2;
bool reading = digitalRead(pin);
unsigned long now = millis();
bool clicked = false;
if(reading != *lastPtr && (now - *debPtr) > DEBOUNCE_MS){
*debPtr = now;
if(reading == LOW){
clicked = true;
}
*lastPtr = reading;
}
return clicked;
}
bool checkLongPressB2(){
bool b2Now = (digitalRead(BTN2)==LOW);
unsigned long now = millis();
if(!b2Held){
if(b2Now){
b2Held=true;
b2EscapeDone=false;
b2PressStart=now;
}
} else {
if(!b2Now){
b2Held=false;
b2EscapeDone=false;
} else {
if(!b2EscapeDone && (now - b2PressStart)>=B2_LONG_MS){
b2EscapeDone=true;
return true;
}
}
}
return false;
}
bool voltCheckExitHold(){
static bool vHeld=false;
static unsigned long vStart=0;
static bool fired=false;
bool nowPressed = (digitalRead(BTN2)==LOW);
unsigned long nowMs = millis();
if(!vHeld){
if(nowPressed){
vHeld=true;
vStart=nowMs;
fired=false;
}
} else {
if(!nowPressed){
vHeld=false;
} else {
if(!fired && (nowMs - vStart)>=B2_LONG_MS){
fired=true;
return true;
}
}
}
return false;
}
#ifndef REPROMOD_COMPAT_FWD_INCLUDED
#define REPROMOD_COMPAT_FWD_INCLUDED
#include <stdint.h>
enum TestChipResult_TC : uint8_t;
void beepOnce(uint16_t ms);
void beepCount(uint8_t n, uint16_t onMs = 80, uint16_t offMs = 80);
extern const unsigned long SAFE_ARM_DELAY_MS;
class Adafruit_SSD1306;
extern Adafruit_SSD1306 display;
void setRelay(uint8_t pin, bool on);
void readVA(float &voltsOut, float &ampsOut);
void registerActivity(void);
extern bool relay1;
extern unsigned long ch1OnMs;
extern bool lastShortTrip;
#endif
bool displaySleeping=false;
unsigned long lastActivityMs=0;
const unsigned long OLED_SLEEP_MS = 20000;
void registerActivity(){
lastActivityMs = millis();
}
void oledOff(){
if(!displaySleeping){
display.ssd1306_command(SSD1306_DISPLAYOFF);
displaySleeping=true;
}
}
void oledOn(){
if(displaySleeping){
display.ssd1306_command(SSD1306_DISPLAYON);
displaySleeping=false;
}
}
bool rawBtnPressed(){
return (digitalRead(BTN1)==LOW) || (digitalRead(BTN2)==LOW);
}
enum UiMode {
MODE_HOME = 0,
MODE_MENU,
MODE_TEST_CHIP,
MODE_VOLT,
MODE_LAPTOP
};
UiMode uiMode = MODE_HOME;
enum MainMenuItem {
MM_CANAL2 = 0,
MM_TESTCHIP,
MM_MEDICION,
MM_LAPTOP,
MM_COUNT
};
uint8_t mainSel = 0;
enum TestChipItem {
TC_CPU = 0,
TC_GPU,
TC_VRAM,
TC_BUCK,
TC_COUNT
};
uint8_t testChipSel = 0;
const uint8_t PW=118;
const uint8_t PH=24;
const uint8_t GX=5;
const uint8_t GY=20;
uint8_t gbuf[PW];
uint8_t head=0;
float aHist[4] = {0,0,0,0};
unsigned long tHist[4] = {0,0,0,0};
uint8_t aHidx = 0;
bool aHistPrimed = false;
const float GRAPH_MAX_DROP = 1.0f;
float vref = 0.0f;
const float VREF_HYST = 0.40f;
const unsigned long VREF_REARM_HOLD_MS=1000;
#define INA_SHUNT_REG 0x01
float readVoltageCal(){
return ina.getBusVoltage() * V_CAL;
}
int16_t inaReadRegister16(uint8_t reg){
Wire.beginTransmission(INA_ADDR);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom(INA_ADDR,(uint8_t)2);
if(Wire.available()<2) return 0;
uint16_t msb=Wire.read();
uint16_t lsb=Wire.read();
uint16_t raw=(msb<<8)|lsb;
return (int16_t)raw;
}
float readShuntVoltage_mV(){
int16_t raw=inaReadRegister16(INA_SHUNT_REG);
return (float)raw * 0.0025f;
}
void readVA(float &voltsOut, float &ampsOut){
float vOut = readVoltageCal();
float shunt_mV = readShuntVoltage_mV();
float shunt_V = shunt_mV / 1000.0f;
float amps = (shunt_V / SHUNT_OHMS) * I_CAL;
voltsOut = vOut;
ampsOut = amps;
}
float aSlow = 0.0f;
float aPeakSwing = 0.0f;
unsigned long lastVoltSampleMs=0;
const float FLUCT_THRESH = 0.01f;
const float PULSE_THRESH = 0.05f;
void drawLaptopAnalysisSimple(const char* msg, float ampsNow, bool relayOn){
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(18, 0);
display.print("ANALISIS");
display.fillRect(0, 16, 160, 14, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setTextSize(1);
display.setCursor(2, 20);
display.print(msg);
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(10, 34);
display.print(fabs(ampsNow), 3);
display.print("A");
float vNow = 0.0f, aNow = 0.0f;
readVA(vNow, aNow);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(90, 40);
int vDisplay = (int)vNow;
int vDec = (int)((vNow - vDisplay) * 10);
if (vDisplay < 10) display.print('0');
display.print(vDisplay);
display.print('.');
display.print(vDec);
display.print('V');
display.fillRect(2, 52, 40, 14, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setTextSize(1);
display.setCursor(5, 54);
display.print("B1 ");
display.print(relayOn ? "ON" : "OFF");
display.setTextColor(SSD1306_WHITE);
display.setCursor(94, 54);
display.print("B2<");
display.display();
}
void drawInvertedLabel(int16_t x, int16_t y, const char *txt){
int16_t bx,by; uint16_t bw,bh;
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.getTextBounds(txt, x, y, &bx,&by,&bw,&bh);
display.fillRect(bx-2, by-1, bw+4, bh+2, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setCursor(x,y);
display.print(txt);
display.setTextColor(SSD1306_WHITE);
}
void drawHomeIdle(){
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(3, 54);
display.print("B1 canal1");
display.setCursor(79, 54);
display.print("B2 menu");
display.setTextColor(SSD1306_WHITE);
{
const char *txt="REPROMOD";
int16_t x1,y1; uint16_t w1,h1;
display.setTextSize(2);
display.getTextBounds(txt,0,0,&x1,&y1,&w1,&h1);
int16_t boxX = (OLED_W - (w1+6)) / 2;
if(boxX<0) boxX=0;
int16_t boxY = 8;
uint16_t boxW = w1+6;
uint16_t boxH = h1+4;
display.fillRect(boxX,boxY,boxW,boxH,SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setCursor(boxX+3, boxY+2);
display.print(txt);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
int16_t vx,vy; uint16_t vw,vh;
display.getTextBounds(FW_VERSION,0,0,&vx,&vy,&vw,&vh);
display.setCursor((OLED_W - vw)/2, boxY+boxH+6);
display.print(FW_VERSION);
}
display.display();
}
void drawTripScreen() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(2, 2);
display.println(F("PROTECCION"));
display.fillRect(0, 28, 128, 14, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setTextSize(1);
display.setCursor(4, 32);
if (lastTripWasPlus) {
display.println(F(" Pico de corriente"));
} else {
display.println(F(" Caida de voltaje"));
}
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(2, 54);
display.println(F("B1> Rearmar"));
display.setCursor(98, 54);
display.println(F("B2<"));
display.display();
}
void drawChannel1Frame(float v, float a, float dropV, float pct, bool safeOn){
display.clearDisplay();
display.fillRect(0,0,128,12,SSD1306_BLACK);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print(v,2); display.print("v ");
display.print(a,2); display.print("A");
const int16_t STATUS_X = 76;
const int16_t STATUS_Y = 0;
const int16_t STATUS_W = 52;
const int16_t STATUS_H = 12;
display.fillRect(STATUS_X, STATUS_Y, STATUS_W, STATUS_H, SSD1306_BLACK);
switch (safeMode) {
case SAFE_V: {
display.setTextColor(SSD1306_WHITE);
display.setCursor(STATUS_X + 12, STATUS_Y);
display.print("SAFE");
} break;
case SAFE_PLUS: {
display.setTextColor(SSD1306_WHITE);
display.setCursor(STATUS_X + 6, STATUS_Y);
display.print("SAFE+");
} break;
case SAFE_OFF: {
static bool blink=false;
static unsigned long t0=0;
unsigned long now=millis();
if (now - t0 > 500) { t0 = now; blink = !blink; }
if (blink) {
display.fillRect(STATUS_X, STATUS_Y, STATUS_W, STATUS_H, SSD1306_WHITE);
display.drawRect(STATUS_X, STATUS_Y, STATUS_W, STATUS_H, SSD1306_BLACK);
display.setTextColor(SSD1306_BLACK);
display.setCursor(STATUS_X + 6, STATUS_Y + 1);
display.print("NO SAFE");
display.setTextColor(SSD1306_WHITE);
} else {
display.setTextColor(SSD1306_WHITE);
display.setCursor(STATUS_X + 6, STATUS_Y);
display.print("NO SAFE");
}
} break;
}
const uint8_t PLOT_TOP = 14;
const uint8_t PLOT_BOTTOM = OLED_H - 18;
const uint8_t plotH = (PLOT_BOTTOM > PLOT_TOP) ? (PLOT_BOTTOM - PLOT_TOP) : 1;
for(uint8_t band=1; band<=4; band++){
uint8_t yGuide = PLOT_TOP + (uint16_t)plotH * band / 5;
for(uint8_t x=GX; x < (uint8_t)(GX+PW); x++){
if((x % 4) < 2){
if(yGuide < OLED_H) display.drawPixel(x, yGuide, SSD1306_WHITE);
}
}
}
{
uint8_t x = GX;
uint8_t idx = head;
for(uint8_t i=0; i<PW; i++){
uint8_t oldY = gbuf[idx];
uint8_t yScaled = (uint8_t)((uint32_t)oldY * (uint32_t)(plotH-1) / (uint32_t)((PH>1)?(PH-1):1));
uint8_t baseY = PLOT_TOP + yScaled;
for(int8_t dy=-1; dy<=1; dy++){
int16_t yy = (int16_t)baseY + dy;
if(yy>=0 && yy<OLED_H) display.drawPixel(x, yy, SSD1306_WHITE);
}
x++;
idx = (idx+1) % PW;
}
}
int mvInt = (int)(dropV*1000.0f+0.5f); if(mvInt>999) mvInt=999;
int pctInt=(int)(pct+0.5f);
if(pctInt<0) pctInt=0; if(pctInt>99) pctInt=99;
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(2, OLED_H-18);
if(mvInt<100) display.print('0');
if(mvInt<10) display.print('0');
display.print(mvInt);
display.print("mV ");
if(pctInt<10) display.print('0');
display.print(pctInt);
display.print("%");
display.display();
}
void drawTestChipMenu_TC(uint8_t sel, TestChipResult_TC pendingRes){
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(30,0);
display.print("TESTER CHIP");
if(sel==TCSEL_CPU){
drawInvertedLabel(10,14,"CPU");
} else {
display.setCursor(10,14); display.print("CPU");
}
if(sel==TCSEL_GPU){
drawInvertedLabel(90,14,"GPU");
} else {
display.setCursor(90,14); display.print("GPU");
}
if(sel==TCSEL_VRAM){
drawInvertedLabel(10,30,"VRAM");
} else {
display.setCursor(10,30); display.print("VRAM");
}
if(sel==TCSEL_BUCK){
drawInvertedLabel(90,30,"BUCK");
} else {
display.setCursor(90,30); display.print("BUCK");
}
display.setCursor(4,42);
display.print("Ajuste 1v a 4A B1>");
drawInvertedLabel(6, 54, " DESCONECTE PINZAS!");
}
void drawTestArmed_TC(uint8_t sel){
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
switch(sel){
case TCSEL_CPU: display.setCursor(0,0); display.print("CPU"); break;
case TCSEL_GPU: display.setCursor(0,0); display.print("GPU"); break;
case TCSEL_VRAM: display.setCursor(0,0); display.print("VRAM"); break;
case TCSEL_BUCK: display.setCursor(0,0); display.print("BUCK"); break;
}
display.setTextSize(1);
display.setCursor(0,24);
display.print("Pinza + en placa");
display.setCursor(0,34);
display.print("Pinza - en GND");
drawInvertedLabel(0,50,"B1 TEST");
display.setCursor(60,50);
display.print("B2<");
display.display();
}
void drawTestMeasuring_TC(uint8_t sel){
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
switch(sel){
case TCSEL_CPU: display.setCursor(0,0); display.print("CPU"); break;
case TCSEL_GPU: display.setCursor(0,0); display.print("GPU"); break;
case TCSEL_VRAM: display.setCursor(0,0); display.print("VRAM"); break;
case TCSEL_BUCK: display.setCursor(0,0); display.print("BUCK"); break;
}
display.setTextSize(1);
display.setCursor(0,28);
display.print("Comprobando V...");
display.setCursor(0,40);
display.print("Mantenga 1v estable");
unsigned long elapsed = millis() - tcMeasureStartMs;
uint8_t barW = (elapsed >= TC_MEASURE_WINDOW_MS)
? 100
: (uint8_t)( (elapsed*100UL)/TC_MEASURE_WINDOW_MS );
display.drawRect(0,56,104,6,SSD1306_WHITE);
display.fillRect(1,57,barW,4,SSD1306_WHITE);
display.display();
}
void drawTestResult_TC(uint8_t sel,
TestChipResult_TC res,
float vAvg,float vMin,float vMax)
{
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
switch(sel){
case TCSEL_CPU: display.setCursor(0,0); display.print("CPU"); break;
case TCSEL_GPU: display.setCursor(0,0); display.print("GPU"); break;
case TCSEL_VRAM: display.setCursor(0,0); display.print("VRAM"); break;
case TCSEL_BUCK: display.setCursor(0,0); display.print("BUCK"); break;
}
display.setTextSize(1);
const char *msgMain = "";
bool invertMsg=false;
switch(res){
case TCR_OK: msgMain="CHIP OK!"; invertMsg=false; break;
case TCR_VLOW: msgMain="V BAJO"; invertMsg=true; break;
case TCR_VHIGH: msgMain="V ALTO"; invertMsg=true; break;
case TCR_SHORT: msgMain="CORTO!"; invertMsg=true; break;
case TCR_NOCON: msgMain="SIN CARGA";invertMsg=true; break;
case TCR_FAIL:
default: msgMain="CHIP FALLA"; invertMsg=true; break;
}
if(invertMsg){
drawInvertedLabel(0,24,msgMain);
}else{
display.setCursor(0,24);
display.print(msgMain);
}
float useMin = (tc_cntStable>0) ? tc_vMinStable : vMin;
float useMax = (tc_cntStable>0) ? tc_vMaxStable : vMax;
float dropPct = 0.0f;
if(useMax > 0.001f){
dropPct = (useMax - useMin)/useMax * 100.0f;
if(dropPct < 0) dropPct = 0;
if(dropPct > 99.0f) dropPct = 99.0f;
}
display.setCursor(0,36);
display.print("Media ");
display.print(vAvg,2);
display.print("v");
display.setCursor(0,46);
display.print("MIN ");
display.print(vMin,2);
display.print("v");
display.setCursor(64,46);
display.print("MAX ");
display.print(vMax,2);
display.print("v");
display.setCursor(0,56);
display.print("Caida ");
display.print(dropPct,0);
display.print("%");
drawInvertedLabel(95,56,"B1>");
display.setCursor(70,56);
display.print("B2<");
display.display();
}
void drawTestSummaryScreen_WithGraph_Compact(
uint8_t sel,
uint8_t finalCode,
float vAvg,
float vMin,
float vMax,
uint8_t dropPct
){
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
const char *chipTxt = "CPU";
switch(sel){
case TCSEL_CPU: chipTxt="CPU"; break;
case TCSEL_GPU: chipTxt="GPU"; break;
case TCSEL_VRAM: chipTxt="VRAM"; break;
case TCSEL_BUCK: chipTxt="BUCK"; break;
}
const char *resTxt = "OK";
bool badRes = false;
switch(finalCode){
case 0: resTxt="OK"; badRes=false; break;
case 1: resTxt="V BAJO"; badRes=true; break;
case 2: resTxt="V ALTO"; badRes=true; break;
case 3: resTxt="CORTO"; badRes=true; break;
case 5: resTxt="SIN CARGA"; badRes=true; break;
default:
case 4: resTxt="INESTABLE"; badRes=true; break;
}
char headerBuf[20];
snprintf(headerBuf,sizeof(headerBuf),"%s %s", chipTxt, resTxt);
display.setTextSize(1);
int16_t hbX, hbY; uint16_t hbW, hbH;
display.getTextBounds(headerBuf, 0,0, &hbX,&hbY,&hbW,&hbH);
int16_t boxX = (OLED_W - (int16_t)hbW - 6) / 2;
if(boxX < 0) boxX = 0;
int16_t boxY = 0;
uint16_t boxW = hbW + 6;
uint16_t boxH = hbH + 4;
display.fillRect(boxX, boxY, boxW, boxH, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setCursor(boxX+3, boxY+2);
display.print(headerBuf);
display.setTextColor(SSD1306_WHITE);
const int GXg = 0;
const int GW = 128;
const int yTop = 16;
const int y1V = 24;
const int y05V = 36;
const int y0V = 52;
const int yText0 = 10;
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, yText0);
display.print("1v");
display.setCursor(0, y05V-3);
display.print("0.5");
display.setCursor(0, y0V-3);
display.print("0v");
for (int x = 12; x < GW; x++) {
if ((x % 4) < 2) {
display.drawPixel(GXg + x, y1V, SSD1306_WHITE);
}
}
for (int x = 12; x < GW; x++) {
if ((x % 4) < 2) {
display.drawPixel(GXg + x, y05V, SSD1306_WHITE);
}
}
for (int x = 12; x < GW; x++) {
display.drawPixel(GXg + x, y0V, SSD1306_WHITE);
}
if (tc_traceCount > 1) {
uint16_t startIdx = (uint16_t)(tc_traceCount * 20UL / 100UL);
if (startIdx >= tc_traceCount - 1) startIdx = 0;
uint16_t usedCount = tc_traceCount - startIdx;
if (usedCount < 2) usedCount = 2;
const float V_TOP = 1.00f;
const float V_MID = 0.50f;
const int yTopTrace = y1V;
const int yBottomTrace = y05V;
int prevX = -1;
int prevY = -1;
for (int x = 12; x < GW; x++) {
uint16_t idx = startIdx + (uint16_t)(
((uint32_t)(x-12) * (uint32_t)usedCount) / (uint32_t)(GW-12)
);
if (idx >= tc_traceCount) idx = tc_traceCount - 1;
float vNow = tc_trace[idx];
if (vNow > V_TOP) vNow = V_TOP;
if (vNow < V_MID) vNow = V_MID;
float norm = (vNow - V_MID) / (V_TOP - V_MID);
int yPix = yBottomTrace
- (int)( norm * (float)(yBottomTrace - yTopTrace) + 0.5f );
if (yPix < yTopTrace) yPix = yTopTrace;
if (yPix > yBottomTrace) yPix = yBottomTrace;
int Xpix = GXg + x;
display.drawPixel(Xpix, yPix, SSD1306_WHITE);
if (prevX >= 0) {
if (abs(yPix - prevY) > 1) {
int yA = (yPix < prevY) ? yPix : prevY;
int yB = (yPix < prevY) ? prevY : yPix;
for (int yy = yA; yy <= yB; yy++) {
display.drawPixel(Xpix, yy, SSD1306_WHITE);
}
}
}
prevX = Xpix;
prevY = yPix;
}
} else {
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(32, (y1V + y05V)/2 - 3);
display.print("SIN DATOS");
}
char footerBuf[48];
int vMax_i = (int)floor(fabs(vMax));
int vMax_d = (int)round((fabs(vMax) - (float)vMax_i) * 100.0f);
if (vMax_d > 99) vMax_d = 99;
int vMin_i = (int)floor(fabs(vMin));
int vMin_d = (int)round((fabs(vMin) - (float)vMin_i) * 100.0f);
if (vMin_d > 99) vMin_d = 99;
int vAvg_i = (int)floor(fabs(vAvg));
int vAvg_d = (int)round((fabs(vAvg) - (float)vAvg_i) * 100.0f);
if (vAvg_d > 99) vAvg_d = 99;
snprintf(
footerBuf,
sizeof(footerBuf),
"%02u%% +%d.%02d -%d.%02d =%d.%02d",
(unsigned int)dropPct,
vMax_i, vMax_d,
vMin_i, vMin_d,
vAvg_i, vAvg_d
);
int16_t fx, fy;
uint16_t fW, fH;
display.getTextBounds(footerBuf, 0, 0, &fx, &fy, &fW, &fH);
int16_t footerY = OLED_H - fH;
int16_t footerX = (OLED_W - fW) / 2;
if (footerX < 0) footerX = 0;
display.setCursor(footerX, footerY);
display.print(footerBuf);
display.display();
}
TestChipResult_TC classifyResult_TC(){
uint16_t useCnt = (tc_cntStable > 0) ? tc_cntStable : tc_cnt;
float useVmin = (tc_cntStable > 0) ? tc_vMinStable : tc_vMin;
float useVmax = (tc_cntStable > 0) ? tc_vMaxStable : tc_vMax;
if(useCnt == 0){
return TCR_FAIL;
}
float vAvg = tc_vSum / (float)tc_cnt;
float aAvg = tc_aSum / (float)tc_cnt;
if (testChipSel_TC == TCSEL_GPU) {
if (aAvg < 0.02f) return TCR_NOCON;
} else {
if (aAvg < 0.05f) return TCR_NOCON;
}
if ((useVmin < 0.60f && aAvg > 0.5f) || (useVmin < 0.35f)) {
return TCR_SHORT;
}
if(useVmax > 1.40f){
return TCR_VHIGH;
}
bool vOk = (vAvg >= 0.80f && vAvg <= 1.20f);
bool aOk = (aAvg >= 0.20f && aAvg <= 3.50f);
if(vOk && aOk){
return TCR_OK;
}
if(vAvg < 0.80f){
return TCR_VLOW;
}
if(vAvg > 1.20f){
return TCR_VHIGH;
}
return TCR_FAIL;
}
const float CPU_SHORT_V = 0.30f;
const float CPU_MIN_AVG_OK_V = 0.75f;
const float CPU_MAX_EXPECTED_V = 1.20f;
const float CPU_DROP_BAD_PCT = 30.0f;
TestChipResult_TC classifyCPU_TC(){
if(tc_cnt == 0){
return TCR_FAIL;
}
float vAvg = tc_vSum / (float)tc_cnt;
float dropPct = 0.0f;
if (tc_vMax > 0.001f) {
dropPct = (tc_vMax - tc_vMin) / tc_vMax * 100.0f;
if(dropPct < 0.0f) dropPct = 0.0f;
}
if (tc_vMin < CPU_SHORT_V){
return TCR_SHORT;
}
if (tc_vMax > CPU_MAX_EXPECTED_V){
return TCR_VHIGH;
}
if (vAvg < CPU_MIN_AVG_OK_V){
return TCR_VLOW;
}
if (dropPct > CPU_DROP_BAD_PCT){
return TCR_FAIL;
}
return TCR_OK;
}
void drawMainMenu(uint8_t sel){
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
if(sel==MM_CANAL2){
drawInvertedLabel(4,10,"CANAL 2");
} else {
display.setCursor(4,10);
display.print("CANAL 2");
}
if(sel==MM_TESTCHIP){
drawInvertedLabel(70,10,"TEST CHIP");
} else {
display.setCursor(70,10);
display.print("TEST CHIP");
}
if(sel==MM_MEDICION){
drawInvertedLabel(4,30,"MEDICION");
} else {
display.setCursor(4,30);
display.print("MEDICION");
}
if(sel==MM_LAPTOP){
drawInvertedLabel(70,30,"PORTATIL");
} else {
display.setCursor(70,30);
display.print("PORTATIL");
}
drawInvertedLabel(4,52,"B1>");
display.setCursor(50,52);
display.print("B2 pasa/sal");
display.display();
}
void drawTestChipMenu(uint8_t sel){
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
if(sel==TC_CPU){
drawInvertedLabel(4,8,"CPU");
} else {
display.setCursor(4,8); display.print("CPU");
}
if(sel==TC_GPU){
drawInvertedLabel(74,8,"GPU");
} else {
display.setCursor(74,8); display.print("GPU");
}
if(sel==TC_VRAM){
drawInvertedLabel(4,24,"VRAM");
} else {
display.setCursor(4,24); display.print("VRAM");
}
if(sel==TC_BUCK){
drawInvertedLabel(74,24,"BUCK");
} else {
display.setCursor(74,24); display.print("BUCK");
}
display.setCursor(4,44);
display.print("Ajuste 1v Pulse B1");
display.setCursor(4,56);
display.print("Volver B2 largo");
display.display();
}
void drawVoltimetro(float vShow, const char *estadoTxt){
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(2,0);
display.print("VOLTIMETRO");
{
char buf[16];
dtostrf(vShow,0,3,buf);
int16_t x1,y1; uint16_t w1,h1;
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.getTextBounds(buf,0,0,&x1,&y1,&w1,&h1);
int16_t xx=(OLED_W - (w1+12))/2;
if(xx<0)xx=0;
int16_t yy=24;
display.setCursor(xx,yy);
display.print(buf);
display.print("V");
}
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10,50);
display.print("Senal ");
display.print(estadoTxt);
drawInvertedLabel(90,50,"B2<");
display.display();
}
void drawVoltimetroExitWarning(){
display.clearDisplay();
display.fillRect(0,0,OLED_W,OLED_H,SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setTextSize(2);
display.setCursor(16,12);
display.print("RECUERDE");
display.setTextSize(1);
display.setCursor(28,36);
display.print("¡DESCONECTE");
display.setCursor(28,48);
display.print("LAS PINZAS!");
display.display();
}
void drawLaptopScreen(bool needAdj19v){
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(20,0);
display.print("PORTATIL");
display.setTextSize(1);
if (needAdj19v) {
display.setCursor(15, 20);
display.print("AJUSTE EL VOLTAJE");
drawInvertedLabel(22, 36, "RANGO 18 A 21V");
display.setCursor(4, 54);
drawInvertedLabel(4,54, "B1>");
display.setCursor(105, 54);
display.print("B2<");
} else {
display.setCursor(4, 16);
display.print("ANALISIS");
display.setTextSize(2);
display.setCursor(0, 30);
display.print("0.000A");
display.setTextSize(1);
display.setCursor(4, 54);
display.print("B1 on/off");
display.setCursor(90, 54);
display.print("B2<");
}
display.display();
}
void drawLaptopAdjustWarning(float vAvg){
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("AVISO");
display.setTextSize(1);
display.setCursor(0, 22);
display.print("Medido: ");
display.print(vAvg, 2);
display.print("V");
display.fillRect(0, 36, 128, 14, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setCursor(8, 38);
display.print("Ajuste 18 a 21V primero");
display.setTextColor(SSD1306_WHITE);
display.setCursor(92, 54);
display.print("B2<");
display.display();
}
void armCh1(){
relay1=true;
setRelay(RELAY1,true);
safeModeEnabled=true;
ch1OnMs=millis();
vref = readVoltageCal();
lastShortTrip=false;
tripScreenDrawn=false;
ch1State=CH1_ACTIVE;
}
void forceGoHome(){
relay1=false;
setRelay(RELAY1,false);
ch1State = CH1_IDLE;
lastShortTrip=false;
tripScreenDrawn=false;
uiMode = MODE_HOME;
}
void redrawCurrentScreenAfterWake() {
uiMode = MODE_HOME;
ch1State = CH1_IDLE;
relay1 = false;
setRelay(RELAY1,false);
drawHomeIdle();
}
bool tcCheckOneVolt_OK() {
relay1 = true;
setRelay(RELAY1, true);
safeModeEnabled = true;
ch1OnMs = millis();
delay(50);
float vNow = 0.0f, aNow = 0.0f;
readVA(vNow, aNow);
if (aNow >= 4.0f) {
relay1 = false;
setRelay(RELAY1, false);
beepCount(3);
lastResult_TC = TCR_SHORT;
testPhase_TC = TCPH_RESULT;
return false;
}
tc_lastCheckVolt = vNow;
relay1 = false;
setRelay(RELAY1, false);
if (vNow >= TC_OK_MIN_V && vNow <= TC_OK_MAX_V) {
return true;
} else {
return false;
}
}
struct TCProfile {
float minV;
float maxV;
float shortV;
float highV;
float dropBadPct;
unsigned long windowMs;
};
TCProfile tcProfile;
void configureProfileForCurrentSelection(uint8_t sel) {
switch (sel) {
case TCSEL_CPU:
tcProfile = {0.80f, 1.20f, 0.30f, 1.40f, 30.0f, 5500};
break;
case TCSEL_GPU:
tcProfile = {0.80f, 1.20f, 0.30f, 1.50f, 25.0f, 5500};
break;
case TCSEL_VRAM:
tcProfile = {1.00f, 1.40f, 0.50f, 1.60f, 20.0f, 3000};
break;
case TCSEL_BUCK:
tcProfile = {0.60f, 2.00f, 0.20f, 2.50f, 50.0f, 500};
break;
default:
tcProfile = {0.80f, 1.20f, 0.30f, 1.40f, 30.0f, 5500};
break;
}
TC_MEASURE_WINDOW_MS = tcProfile.windowMs;
}
void handleTestChip_TC(){
if(checkLongPressB2()){
beepOnce(40);
setRelay(RELAY1,false);
relay1=false;
ch1State = CH1_IDLE;
uiMode = MODE_HOME;
return;
}
switch(testPhase_TC){
case TCPH_READY: {
drawTestChipMenu_TC(testChipSel_TC, lastResult_TC);
display.display();
if (consumeClick(BTN2)) {
beepOnce(40);
testChipSel_TC = (testChipSel_TC + 1) % TCSEL_COUNT;
drawTestChipMenu_TC(testChipSel_TC, lastResult_TC);
display.display();
registerActivity();
break;
}
if (consumeClick(BTN1)) {
relay1 = true;
setRelay(RELAY1, true);
delay(100);
float vNow = 0.0f, aNow = 0.0f;
readVA(vNow, aNow);
tc_lastAdjV = vNow;
tc_lastAdjA = aNow;
delay(200);
relay1 = false;
setRelay(RELAY1, false);
float vTarget = 1.0f;
if (testChipSel_TC == TCSEL_VRAM) {
vTarget = 1.20f;
}
float vMinOk = vTarget * 0.90f;
float vMaxOk = vTarget * 1.10f;
if (vNow >= vMinOk && vNow <= vMaxOk) {
switch (testChipSel_TC) {
case TCSEL_GPU: tc_runMs = 6000; tc_settleMs = 300; break;
case TCSEL_VRAM: tc_runMs = 4000; tc_settleMs = 200; break;
case TCSEL_BUCK: tc_runMs = 300; tc_settleMs = 100; break;
default: tc_runMs = 4000; tc_settleMs = 200; break;
}
beepOnce(80);
testPhase_TC = TCPH_ARMED;
break;
}
beepCount(2, 60, 80);
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(30,0);
display.print("TESTER CHIP");
if (testChipSel_TC == TCSEL_VRAM) {
drawInvertedLabel(4,48,"AJUSTE 1.2V PRIMERO");
display.setCursor(4,28);
display.print("Medido: ");
display.print(tc_lastAdjV, 3);
display.print("V");
} else {
drawInvertedLabel(4,48,"AJUSTE 1V PRIMERO");
display.setCursor(4,28);
display.print("Medido: ");
display.print(tc_lastAdjV, 3);
display.print("V");
}
display.display();
display.display();
delaySafe(6000);
break;
break;
}
} break;
case TCPH_ARMED: {
drawTestArmed_TC(testChipSel_TC);
if (consumeClick(BTN2)) {
beepOnce(40);
testPhase_TC = TCPH_READY;
return;
}
if(consumeClick(BTN1)){
beepOnce(80);
tc_vMin = 99.0f;
tc_vMax = 0.0f;
tc_vSum = 0.0f;
tc_cnt = 0;
tc_aMinStable = 99.0f;
tc_aMaxStable = 0.0f;
tc_vMinStable = 99.0f;
tc_vMaxStable = 0.0f;
tc_aSumStable = 0.0f;
tc_cntStable = 0;
tc_aPrev = NAN;
tc_aP2P = 0.0f;
tc_aStepMax = 0.0f;
for (uint16_t i = 0; i < TC_TRACE_MAX; i++) {
tc_trace[i] = 0.0f;
}
tc_traceCount = 0;
relay1=true;
setRelay(RELAY1,true);
safeModeEnabled=true;
ch1OnMs=millis();
tcMeasureStartMs=millis();
testPhase_TC = TCPH_MEASURE;
return;
}
configureProfileForCurrentSelection(testChipSel_TC);
} break;
case TCPH_MEASURE: {
const unsigned long WARMUP_IGNORE_MS = 50;
const unsigned long STABLE_START_MS = 200;
unsigned long now = millis();
unsigned long elapsed = now - tcMeasureStartMs;
float vNow = 0.0f;
float aNow = 0.0f;
readVA(vNow, aNow);
if (elapsed < (tcMeasureStartMs + (TC_MEASURE_WINDOW_MS / 2))) {
tc_aSumEarly += aNow;
tc_aCntEarly++;
} else {
tc_aSumLate += aNow;
tc_aCntLate++;
}
if (aNow > tc_aMax) tc_aMax = aNow;
if (aNow < tc_aMin) tc_aMin = aNow;
if (!isnan(tc_aPrev)) {
float step = fabs(aNow - tc_aPrev);
if (step > tc_aStepMax) tc_aStepMax = step;
}
tc_aPrev = aNow;
if (tc_traceCount < TC_TRACE_MAX) {
tc_trace[tc_traceCount] = vNow;
tc_traceCount++;
}
if (elapsed >= WARMUP_IGNORE_MS){
if (vNow < tc_vMin) tc_vMin = vNow;
if (vNow > tc_vMax) tc_vMax = vNow;
tc_vSum += vNow;
if (aNow < tc_aMin) tc_aMin = aNow;
if (aNow > tc_aMax) tc_aMax = aNow;
tc_aSum += aNow;
if (aNow < tc_aMinStable) tc_aMinStable = aNow;
if (aNow > tc_aMaxStable) tc_aMaxStable = aNow;
tc_cnt++;
}
if (elapsed >= STABLE_START_MS){
if (vNow < tc_vMinStable) tc_vMinStable = vNow;
if (vNow > tc_vMaxStable) tc_vMaxStable = vNow;
tc_aSumStable += aNow;
tc_cntStable++;
if (tc_stableStartMs == 0) tc_stableStartMs = millis();
unsigned long stableMs = millis() - tc_stableStartMs;
const unsigned long STABLE_SPLIT_MS = 400;
if (stableMs < STABLE_SPLIT_MS) {
tc_aSumEarly += aNow; tc_aCntEarly++;
} else {
tc_aSumLate += aNow; tc_aCntLate++;
}
}
if (safeModeEnabled){
if ((now - ch1OnMs) >= SAFE_ARM_DELAY_MS){
if (vNow < TC_SHORT_V){
relay1 = false;
setRelay(RELAY1,false);
beepCount(3);
lastResult_TC = TCR_SHORT;
tc_finalCode = 3;
testPhase_TC = TCPH_RESULT;
return;
}
}
}
drawTestMeasuring_TC(testChipSel_TC);
if (elapsed >= TC_MEASURE_WINDOW_MS){
relay1 = false;
setRelay(RELAY1,false);
if (testChipSel_TC == TCSEL_CPU || testChipSel_TC == TCSEL_GPU) {
uint16_t useCnt = (tc_cntStable > 0) ? tc_cntStable : tc_cnt;
float useVmin = (tc_cntStable > 0) ? tc_vMinStable : tc_vMin;
float useVmax = (tc_cntStable > 0) ? tc_vMaxStable : tc_vMax;
float vAvg_all = (tc_cnt>0) ? (tc_vSum / (float)tc_cnt) : 0.0f;
float aAvg_all = (tc_cnt>0) ? (tc_aSum / (float)tc_cnt) : 0.0f;
float aEarly = (tc_aCntEarly>0) ? (tc_aSumEarly/(float)tc_aCntEarly) : 0.0f;
float aLate = (tc_aCntLate >0) ? (tc_aSumLate /(float)tc_aCntLate ) : aEarly;
if (aEarly >= 0.30f && aLate <= 0.05f && useVmin >= tcProfile.minV*0.95f) {
lastResult_TC = TCR_FAIL;
}
else if (tc_aStepMax >= 0.50f) {
lastResult_TC = TCR_FAIL;
}
float dropPct = 0.0f;
if (useVmax > 0.001f) {
dropPct = (useVmax - useVmin) / useVmax * 100.0f;
if (dropPct < 0.0f) dropPct = 0.0f;
if (dropPct > 99.0f) dropPct = 99.0f;
}
float aEarlyCPU = (tc_aCntEarly > 0) ? (tc_aSumEarly / (float)tc_aCntEarly) : 0.0f;
float aLateCPU = (tc_aCntLate > 0) ? (tc_aSumLate / (float)tc_aCntLate ) : 0.0f;
float aDropCPU = aEarlyCPU - aLateCPU;
float aP2P_CPU = tc_aMax - tc_aMin;
const float EARLY_MIN_A = 0.20f;
const float LATE_NEAR_ZERO = 0.05f;
const float DROP_MIN_A = 0.15f;
const float STEP_BAD_A = 0.35f;
const float P2P_BAD_A = 0.45f;
const float DROP_V_HELP = 10.0f;
if ( (aEarlyCPU >= EARLY_MIN_A && aLateCPU <= LATE_NEAR_ZERO) ||
(aDropCPU >= DROP_MIN_A) ||
(tc_aStepMax >= STEP_BAD_A) ||
(aP2P_CPU >= P2P_BAD_A && dropPct >= DROP_V_HELP) ) {
lastResult_TC = TCR_FAIL;
}
const float NO_LOAD_A = 0.02f;
const float MIN_A_CPU = 0.30f;
const float MIN_A_GPU = 0.50f;
const float MIN_A_OK = (testChipSel_TC==TCSEL_GPU) ? MIN_A_GPU : MIN_A_CPU;
if (aAvg_all <= 0.005f) {
lastResult_TC = TCR_NOCON;
}
else if ((useVmin < 0.60f && aAvg_all > 0.5f) || (useVmin < 0.35f)) {
lastResult_TC = TCR_SHORT;
}
else if (useVmax > tcProfile.highV) {
lastResult_TC = TCR_VHIGH;
}
else if (vAvg_all < tcProfile.minV) {
lastResult_TC = TCR_VLOW;
}
else if (vAvg_all > tcProfile.maxV) {
lastResult_TC = TCR_VHIGH;
}
else if (aAvg_all < MIN_A_OK) {
lastResult_TC = TCR_FAIL;
}
else if (dropPct > tcProfile.dropBadPct) {
lastResult_TC = TCR_FAIL;
}
else {
lastResult_TC = TCR_OK;
}
{
float aP2P = tc_aMax - tc_aMin;
float rel = (aAvg_all > 0.0f) ? (aP2P / aAvg_all) : 0.0f;
if (rel > 0.55f && dropPct > 6.0f) {
lastResult_TC = TCR_FAIL;
}
}
}
else if (testChipSel_TC == TCSEL_VRAM) {
float vAvg_all = (tc_cnt>0) ? (tc_vSum/(float)tc_cnt) : 0.0f;
float aAvg_all = (tc_cnt>0) ? (tc_aSum/(float)tc_cnt) : 0.0f;
if (aAvg_all < 0.02f) lastResult_TC = TCR_NOCON;
else if (tc_vMin < 0.50f) lastResult_TC = TCR_SHORT;
else if (tc_vMax > 1.60f) lastResult_TC = TCR_VHIGH;
else if (vAvg_all < 1.10f) lastResult_TC = TCR_VLOW;
else if (vAvg_all > 1.30f) lastResult_TC = TCR_VHIGH;
else lastResult_TC = TCR_OK;
}
else {
uint16_t useCnt = (tc_cntStable > 0) ? tc_cntStable : tc_cnt;
float useVmin = (tc_cntStable > 0) ? tc_vMinStable : tc_vMin;
float useVmax = (tc_cntStable > 0) ? tc_vMaxStable : tc_vMax;
float vAvg_all = (tc_cnt > 0) ? (tc_vSum / (float)tc_cnt) : 0.0f;
float aAvg_all = (tc_cnt > 0) ? (tc_aSum / (float)tc_cnt) : 0.0f;
float aP2P = tc_aMax - tc_aMin;
float dropPct = 0.0f;
if (useVmax > 0.001f) {
dropPct = (useVmax - useVmin) / useVmax * 100.0f;
if (dropPct < 0.0f) dropPct = 0.0f;
if (dropPct > 99.0f) dropPct = 99.0f;
}
const float NOLOAD_A = 0.02f;
const float OK_V_MIN = 0.85f;
const float OK_V_MAX = 1.20f;
const float OK_DROP_MAX = 20.0f;
const float FLUCT_A_STRONG = 0.40f;
const float FLUCT_A_MED = 0.25f;
const float UNSTABLE_DROP = 25.0f;
const float SHORT_A_SPIKE = 3.0f;
const float SHORT_V_ABS = 0.60f;
if (aAvg_all < NOLOAD_A && dropPct < 3.0f) {
lastResult_TC = TCR_NOCON;
}
else if ( (tc_aMax >= SHORT_A_SPIKE && useVmin < SHORT_V_ABS) ||
(dropPct >= 55.0f && aAvg_all > 0.5f) ) {
lastResult_TC = TCR_SHORT;
}
else if (aP2P >= FLUCT_A_STRONG) {
lastResult_TC = TCR_FAIL;
}
else if (aP2P >= FLUCT_A_MED && dropPct >= UNSTABLE_DROP) {
lastResult_TC = TCR_FAIL;
}
else if (vAvg_all >= OK_V_MIN && vAvg_all <= OK_V_MAX && dropPct <= OK_DROP_MAX) {
if (aAvg_all >= 0.04f && aAvg_all <= 3.0f) {
lastResult_TC = TCR_OK;
} else {
lastResult_TC = TCR_FAIL;
}
}
else if (vAvg_all < OK_V_MIN) {
lastResult_TC = TCR_VLOW;
}
else if (vAvg_all > OK_V_MAX) {
lastResult_TC = TCR_VHIGH;
}
else {
lastResult_TC = TCR_FAIL;
}
}
if (tc_cntStable >= 12 && lastResult_TC != TCR_SHORT && lastResult_TC != TCR_NOCON) {
float aP2P_stable = tc_aMaxStable - tc_aMinStable;
const float TH_CPU_GPU = 0.45f;
const float TH_VRAM_BUCK = 0.30f;
bool isCpuGpu = (testChipSel_TC == TCSEL_CPU || testChipSel_TC == TCSEL_GPU);
float th = isCpuGpu ? TH_CPU_GPU : TH_VRAM_BUCK;
float vAvg_all = (tc_cnt>0) ? (tc_vSum / (float)tc_cnt) : 0.0f;
if (vAvg_all >= 0.75f && vAvg_all <= 1.25f) {
if (aP2P_stable >= th) {
lastResult_TC = TCR_FAIL;
}
}
}
switch(lastResult_TC){
case TCR_OK: tc_finalCode = 0; break;
case TCR_VLOW: tc_finalCode = 1; break;
case TCR_VHIGH: tc_finalCode = 2; break;
case TCR_SHORT: tc_finalCode = 3; break;
case TCR_NOCON: tc_finalCode = 5; break;
default: tc_finalCode = 4; break;
}
testPhase_TC = TCPH_RESULT;
return;
}
} break;
case TCPH_RESULT: {
float vAvg = (tc_cnt>0) ? (tc_vSum/(float)tc_cnt) : 0.0f;
float dropPctF = 0.0f;
if(tc_vMax>0.001f){
dropPctF = (tc_vMax - tc_vMin)/tc_vMax * 100.0f;
if(dropPctF<0) dropPctF=0;
}
uint8_t dropPct = (dropPctF>99.0f)?99:(uint8_t)(dropPctF+0.5f);
drawTestSummaryScreen_WithGraph_Compact(
testChipSel_TC,
tc_finalCode,
vAvg,
tc_vMin,
tc_vMax,
dropPct
);
if (consumeClick(BTN1)) {
beepOnce(80);
tc_vMin = 99.0f;
tc_vMax = 0.0f;
tc_vSum = 0.0f;
tc_cnt = 0;
tc_aMin = 99.0f;
tc_aMax = 0.0f;
tc_aSum = 0.0f;
tc_aMinStable = 99.0f;
tc_aMaxStable = 0.0f;
tc_vMinStable = 99.0f;
tc_vMaxStable = 0.0f;
tc_aSumStable = 0.0f;
tc_cntStable = 0;
tc_aPrev = NAN;
tc_aP2P = 0.0f;
tc_aStepMax = 0.0f;
for (uint16_t i = 0; i < TC_TRACE_MAX; i++) {
tc_trace[i] = 0.0f;
}
tc_traceCount = 0;
lastResult_TC = TCR_NONE;
tc_finalCode = 4;
relay1 = true;
setRelay(RELAY1, true);
safeModeEnabled = true;
ch1OnMs = millis();
tcMeasureStartMs = millis();
testPhase_TC = TCPH_MEASURE;
return;
}
if (consumeClick(BTN2)) {
beepOnce(40);
relay1 = false;
setRelay(RELAY1,false);
tc_vMin = 99.0f;
tc_vMax = 0.0f;
tc_vSum = 0.0f;
tc_cnt = 0;
tc_aMinStable = 99.0f;
tc_aMaxStable = 0.0f;
tc_aMin = 99.0f;
tc_aMax = 0.0f;
tc_aSum = 0.0f;
tc_vMinStable = 99.0f;
tc_vMaxStable = 0.0f;
tc_aSumStable = 0.0f;
tc_cntStable = 0;
tc_aPrev = NAN;
tc_aP2P = 0.0f;
tc_aStepMax = 0.0f;
for (uint16_t i = 0; i < TC_TRACE_MAX; i++) {
tc_trace[i] = 0.0f;
}
tc_traceCount = 0;
lastResult_TC = TCR_NONE;
tc_finalCode = 4;
testPhase_TC = TCPH_READY;
return;
}
} break;
}
}
void handleLaptop_TC(){
if (laptopPhase == LAPH_ADJ) {
if (consumeClick(BTN2)) {
beepOnce(40);
relay1 = false; setRelay(RELAY1, false);
ch1State = CH1_IDLE;
uiMode = MODE_MENU;
registerActivity();
return;
}
if (consumeClick(BTN1)) {
beepOnce(80);
relay1 = true; setRelay(RELAY1, true);
safeModeEnabled = true;
ch1OnMs = millis();
unsigned long t0 = millis();
float vSum = 0.0f;
uint16_t vCnt = 0;
float vNow = 0.0f, aNow = 0.0f;
while (millis() - t0 < 1000) {
readVA(vNow, aNow);
vSum += vNow;
vCnt++;
delay(10);
}
relay1 = false; setRelay(RELAY1, false);
float vAvg = (vCnt ? (vSum / (float)vCnt) : 0.0f);
if (vAvg < LAPTOP_VMIN || vAvg > LAPTOP_VMAX) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2); display.setCursor(15, 0); display.print("ATENCION");
display.setTextSize(1); display.setCursor(18, 24);
display.print("Medido: "); display.print(vAvg, 2); display.print("V");
drawInvertedLabel(17, 40, "AJUSTE 18 a 21V");
display.display();
display.display();
delaySafe(3500);
return;
return;
}
laptopPhase = LAPH_ANALYSIS;
registerActivity();
return;
}
drawLaptopScreen(true);
return;
}
{
if (consumeClick(BTN2)) {
beepOnce(40);
relay1 = false; setRelay(RELAY1, false);
ch1State = CH1_IDLE;
uiMode = MODE_MENU;
registerActivity();
return;
}
if (consumeClick(BTN1)) {
beepOnce(60);
relay1 = !relay1;
setRelay(RELAY1, relay1);
}
float vNow = 0.0f, aNow = 0.0f;
readVA(vNow, aNow);
if (relay1 && !lap_lastRelayOn) {
lap_vBase = vNow;
lap_aFast = aNow;
lap_aSlow = aNow;
lap_aSwingPeak = 0.0f;
}
lap_lastRelayOn = relay1;
unsigned long nowMs = millis();
if (nowMs - lap_lastSampleMs >= 30) {
lap_lastSampleMs = nowMs;
lap_aFast += (aNow - lap_aFast) * 0.40f;
lap_aSlow += (aNow - lap_aSlow) * 0.05f;
float swing = fabs(lap_aFast - lap_aSlow);
if (swing > lap_aSwingPeak) lap_aSwingPeak = swing;
else lap_aSwingPeak *= 0.92f;
if (aNow >= L_STBY_MIN_A) {
lap_wasAboveStandby = true;
lap_lastAboveTs = nowMs;
}
}
const char* diagMsg = "Analisis activo";
if (!relay1) {
diagMsg = "Analisis pausado";
drawLaptopAnalysisSimple(diagMsg, aNow, relay1);
return;
}
if (vNow > LAPTOP_VMAX || vNow < LAPTOP_VMIN) {
diagMsg = "V FUERA RANGO";
drawLaptopAnalysisSimple(diagMsg, aNow, relay1);
return;
}
if ( (aNow >= L_SHORT_I) ||
(lap_vBase > 0.1f && vNow < (lap_vBase * (1.0f - L_SHORT_DROP_P)) && aNow > 0.10f) ) {
diagMsg = "CORTO!";
drawLaptopAnalysisSimple(diagMsg, aNow, relay1);
return;
}
if (aNow < L_NO_POWER_A) {
if (lap_wasAboveStandby && (nowMs - lap_lastAboveTs) < 1500) {
diagMsg = "SE APAGA";
} else {
diagMsg = "SIN ALIMENTACION";
}
drawLaptopAnalysisSimple(diagMsg, aNow, relay1);
return;
}
if (aNow >= L_STBY_MIN_A && aNow <= L_STBY_MAX_A && lap_aSwingPeak < L_COMM_SWING_A) {
diagMsg = "STANDBY OK";
drawLaptopAnalysisSimple(diagMsg, aNow, relay1);
return;
}
if (lap_aSwingPeak >= L_COMM_SWING_A && aNow < L_BOOT_A) {
diagMsg = "COMUNICACION OK";
drawLaptopAnalysisSimple(diagMsg, aNow, relay1);
return;
}
if (aNow >= L_BOOT_A) {
diagMsg = "INTENTO ARRANQUE";
drawLaptopAnalysisSimple(diagMsg, aNow, relay1);
return;
}
diagMsg = "ALIMENTANDO...";
drawLaptopAnalysisSimple(diagMsg, aNow, relay1);
return;
}
}
void handleHomeMode(){
if(checkLongPressB2()){
beepOnce(40);
forceGoHome();
registerActivity();
return;
}
switch(ch1State){
case CH1_IDLE:{
if(consumeClick(BTN1)){
if(!displaySleeping){
beepOnce(50);
armCh1();
registerActivity();
return;
}
if (consumeClick(BTN2)) {
beepOnce(40);
setRelay(RELAY1, false);
relay1 = false;
ch1State = CH1_IDLE;
uiMode = MODE_HOME;
registerActivity();
return;
}
}
if(consumeClick(BTN2)){
if(!displaySleeping){
beepOnce(40);
uiMode=MODE_MENU;
registerActivity();
return;
}
}
if(!displaySleeping){
drawHomeIdle();
}
delay(40);
} break;
case CH1_ACTIVE:{
if(consumeClick(BTN1)){
if(!displaySleeping){
beepOnce(50);
relay1=false;
setRelay(RELAY1,false);
ch1State=CH1_IDLE;
registerActivity();
return;
}
}
if (consumeClick(BTN2)){
if(!displaySleeping){
safeMode = (SafeMode)((((int)safeMode) + 1) % 3);
switch(safeMode){
case SAFE_V: beepOnce(80); break;
case SAFE_OFF: beepOnce(20); break;
case SAFE_PLUS: beepCount(2,40,60); break;
}
ch1OnMs = millis();
registerActivity();
}
}
float vNow=0.0f, aNow=0.0f;
readVA(vNow,aNow);
unsigned long nowMs = millis();
if (safeMode != SAFE_OFF){
if ((nowMs - ch1OnMs) >= SAFE_ARM_DELAY_MS) {
if (vNow < SAFE_SHORT_VOLTAGE) {
relay1=false; setRelay(RELAY1,false);
lastTripWasPlus = false;
beepCount(3);
lastShortTrip=true;
ch1State=CH1_TRIPPED;
registerActivity();
return;
}
}
}
const float I_BASE = 0.06f;
const float I_SPIKE = 1.10f;
const float I_ABS_HARD = 1.50f;
const unsigned long I_ABS_CONFIRM_MS = 4;
const unsigned long I_IGN_START_MS = 6;
const float DI_DT = 0.80f;
if (safeMode == SAFE_PLUS){
if ((nowMs - ch1OnMs) >= (SAFE_ARM_DELAY_MS + I_IGN_START_MS)) {
aHist[aHidx] = aNow;
tHist[aHidx] = nowMs;
aHidx = (aHidx + 1) & 0x03;
if (!aHistPrimed && aHidx==0) aHistPrimed = true;
if (aHistPrimed){
int i0 = (aHidx + 3) & 0x03;
int i1 = (aHidx + 2) & 0x03;
float a_now = aHist[i0];
float a_prev = aHist[i1];
unsigned long dt = tHist[i0] - tHist[i1];
float di = a_now - a_prev;
bool jumpFast = ((a_now >= I_SPIKE) && (dt <= 40)) || ((di >= DI_DT) && (dt <= 40));
bool absHigh = (a_now >= I_ABS_HARD);
static unsigned long absStartMs = 0;
if (absHigh){
if (absStartMs == 0) absStartMs = nowMs;
} else {
absStartMs = 0;
}
bool absConfirmed = (absStartMs && (nowMs - absStartMs) >= I_ABS_CONFIRM_MS);
if (jumpFast || absConfirmed){
relay1=false; setRelay(RELAY1,false);
lastTripWasPlus = true;
beepCount(3);
lastShortTrip=true;
ch1State=CH1_TRIPPED;
registerActivity();
return;
}
}
}
}
static unsigned long diffStart=0;
unsigned long now=millis();
float dvfabs=fabs(vNow - vref);
if(dvfabs > VREF_HYST){
if(diffStart==0){
diffStart=now;
} else if(now-diffStart>=VREF_REARM_HOLD_MS){
vref=vNow;
diffStart=0;
}
} else {
diffStart=0;
}
float dropV = vref - vNow;
if(dropV<0) dropV=0;
float pctDrop=(vref>0.001f)?(dropV/vref*100.0f):0.0f;
if(pctDrop>=99.0f){
beepOnce(60);
}
float zoomFactor;
if (dropV < 0.10f) {
zoomFactor = 4.0f;
} else if (dropV < 0.20f) {
zoomFactor = 2.5f;
} else if (dropV < 0.40f) {
zoomFactor = 1.5f;
} else {
zoomFactor = 1.0f;
}
float scaledDrop = dropV * zoomFactor;
if(scaledDrop > GRAPH_MAX_DROP) scaledDrop = GRAPH_MAX_DROP;
if(scaledDrop < 0) scaledDrop = 0;
float norm = scaledDrop / GRAPH_MAX_DROP;
uint8_t yVal = (uint8_t)(norm * (PH-1) + 0.5f);
gbuf[head] = yVal;
head = (head+1) % PW;
if(!displaySleeping){
drawChannel1Frame(vNow,aNow,dropV,pctDrop,safeModeEnabled);
}
delay(80);
} break;
case CH1_TRIPPED: {
if (consumeClick(BTN1)) {
if (!displaySleeping) {
beepOnce(50);
armCh1();
registerActivity();
return;
}
}
if (consumeClick(BTN2)) {
if (!displaySleeping) {
beepOnce(40);
setRelay(RELAY1, false);
relay1 = false;
ch1State = CH1_IDLE;
uiMode = MODE_HOME;
registerActivity();
return;
}
}
if (!displaySleeping) {
drawTripScreen();
}
delay(40);
} break;
}
}
void handleMainMenu(){
if(checkLongPressB2()){
beepOnce(40);
forceGoHome();
registerActivity();
return;
}
if(consumeClick(BTN2)){
if(!displaySleeping){
beepOnce(40);
mainSel = (mainSel+1)%MM_COUNT;
registerActivity();
}
}
if(consumeClick(BTN1)){
if(!displaySleeping){
beepOnce(80);
registerActivity();
if(mainSel==MM_CANAL2){
relay2 = !relay2;
setRelay(RELAY2, relay2);
forceGoHome();
return;
} else if (mainSel == MM_TESTCHIP) {
relay1 = false;
setRelay(RELAY1, false);
ch1State = CH1_IDLE;
testChipSel = 0;
testChipSel_TC = TCSEL_CPU;
lastResult_TC = TCR_NONE;
testPhase_TC = TCPH_READY;
tc_vMin = 99.0f;
tc_vMax = 0.0f;
tc_vSum = 0.0f;
tc_cnt = 0;
tcMeasureStartMs = 0;
safeModeEnabled = true;
ch1OnMs = millis();
uiMode = MODE_TEST_CHIP;
registerActivity();
drawTestChipMenu_TC(testChipSel_TC, lastResult_TC);
display.display();
return;
}
else if(mainSel==MM_LAPTOP){
relay1=false; setRelay(RELAY1,false);
ch1State=CH1_IDLE;
laptopPhase = LAPH_ADJ;
uiMode=MODE_LAPTOP;
return;
}
else if(mainSel==MM_MEDICION){
relay1=false; setRelay(RELAY1,false);
ch1State=CH1_IDLE;
uiMode=MODE_VOLT;
return;
} else if(mainSel==MM_LAPTOP){
relay1=false; setRelay(RELAY1,false);
ch1State=CH1_IDLE;
uiMode=MODE_LAPTOP;
return;
}
}
}
if(!displaySleeping){
drawMainMenu(mainSel);
}
delay(60);
}
void handleTestChipMenu(){
if(checkLongPressB2()){
beepOnce(40);
forceGoHome();
registerActivity();
return;
}
if(consumeClick(BTN2)){
if(!displaySleeping){
beepOnce(40);
testChipSel = (testChipSel+1)%TC_COUNT;
registerActivity();
}
}
if(consumeClick(BTN1)){
if(!displaySleeping){
beepOnce(80);
registerActivity();
}
}
if(!displaySleeping){
drawTestChipMenu(testChipSel);
}
delay(60);
}
void handleVoltimetro(){
if (consumeClick(BTN2)){
beepOnce(60);
drawVoltimetroExitWarning();
beepLongWarning();
registerActivity();
forceGoHome();
return;
}
static float vSlow=0.0f;
static float vFast=0.0f;
static float vPeakSwing=0.0f;
static unsigned long winStart=0;
static uint8_t phase=0;
static uint8_t lastState=255;
static float vShow=0.0f;
static uint8_t pendingState=255;
static unsigned long stateStart=0;
const float FLUCT_THRESH_V=0.020f;
const float PULSE_THRESH_V=0.250f;
const float DV_STEP=0.120f;
const float PULSE_RATE=0.25f;
const unsigned long STABLE_TIME_MS=1000UL;
if (phase==0){
if (winStart==0){
winStart=micros();
vPeakSwing=0.0f;
vFast=0.0f;
vSlow=0.0f;
}
float vPrev=0.0f;
bool havePrev=false;
unsigned long n=0, spikes=0;
float vMin=999.0f, vMax=0.0f, vSum=0.0f;
while ((unsigned long)(micros()-winStart) < 30000UL){
float vNow=0.0f, aDummy=0.0f;
readVA(vNow,aDummy);
if (vFast==0.0f && vSlow==0.0f){ vFast=vNow; vSlow=vNow; }
vFast += (vNow - vFast)*0.5f;
vSlow += (vNow - vSlow)*0.05f;
float swing=fabs(vFast - vSlow);
if (swing>vPeakSwing) vPeakSwing=swing;
if (!havePrev){ vPrev=vNow; havePrev=true; }
else { if (fabs(vNow - vPrev)>DV_STEP) spikes++; vPrev=vNow; }
if (vNow<vMin) vMin=vNow;
if (vNow>vMax) vMax=vNow;
vSum+=vNow;
n++;
}
float p2p=(vMax - vMin);
float rate=(n>0)?(float)spikes/(float)n:0.0f;
uint8_t state;
if ((p2p>PULSE_THRESH_V) && (rate>PULSE_RATE)) state=2;
else if (p2p>FLUCT_THRESH_V || rate>(PULSE_RATE*0.5f)) state=1;
else state=0;
unsigned long now=millis();
if (state!=pendingState){
pendingState=state;
stateStart=now;
}
if ((now - stateStart)>=STABLE_TIME_MS && state!=lastState){
if (state==2) beepCount(3);
else if (state==1) beepCount(2);
else beepOnce(60);
lastState=state;
}
vShow=(n>0)?(vSum/(float)n):vSlow;
phase=1;
} else {
if(!displaySleeping){
const char *estadoTxt;
if (lastState==2) estadoTxt="PULSANTE";
else if (lastState==1) estadoTxt="FLUCTUANTE";
else estadoTxt="ESTABLE";
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(8,0);
display.print("VOLTIMETRO");
char buf[16];
dtostrf(vShow,0,3,buf);
int16_t x1,y1; uint16_t w1,h1;
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.getTextBounds(buf,0,0,&x1,&y1,&w1,&h1);
int16_t xx=(OLED_W - (w1+12))/2; if(xx<0)xx=0;
int16_t yy=24;
display.setCursor(xx,yy);
display.print(buf);
display.print("V");
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10,50);
display.print("V ");
display.print(estadoTxt);
drawInvertedLabel(90,50,"B2<");
display.display();
}
phase=0;
winStart=0;
}
}
void handleLaptop(){
if(checkLongPressB2()){
beepOnce(40);
forceGoHome();
registerActivity();
return;
}
bool needAdj19v = true;
if(consumeClick(BTN1)){
if(!displaySleeping){
beepOnce(80);
registerActivity();
}
}
if(!displaySleeping){
drawLaptopScreen(needAdj19v);
}
delay(60);
}
void handleOledSleep(){
unsigned long now = millis();
if(displaySleeping){
if(rawBtnPressed()){
oledOn();
beepOnce(30);
redrawCurrentScreenAfterWake();
wakeSwallowInputs = true;
registerActivity();
}
return;
}
if(uiMode == MODE_HOME && ch1State == CH1_IDLE){
if((now - lastActivityMs) >= OLED_SLEEP_MS){
oledOff();
return;
}
}
}
void setup(){
ina.begin();
ina.setMaxCurrentShunt(16.0f, 0.005f);
pinMode(RELAY1,OUTPUT);
pinMode(RELAY2,OUTPUT);
setRelay(RELAY1,false); relay1=false;
setRelay(RELAY2,false); relay2=false;
pinMode(BTN1,INPUT_PULLUP);
pinMode(BTN2,INPUT_PULLUP);
pinMode(BEEPER,OUTPUT);
noTone(BEEPER);
Wire.begin();
Wire.setClock(100000);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.setRotation(2);
display.clearDisplay();
display.display();
ina.begin();
wdt_enable(WDTO_2S);
vref = readVoltageCal();
for(uint8_t i=0;i<PW;i++) gbuf[i]=0;
head=0;
uiMode = MODE_HOME;
ch1State = CH1_IDLE;
mainSel = 0;
testChipSel = 0;
safeModeEnabled = true;
lastShortTrip=false;
tripScreenDrawn=false;
ch1OnMs=0;
aSlow=0.0f;
aPeakSwing=0.0f;
lastVoltSampleMs=millis();
displaySleeping=false;
lastActivityMs=millis();
drawHomeIdle();
}
void loop(){
wdt_reset();
handleOledSleep();
repromod_dualButtonResetHandler();
if(displaySleeping){
delay(10);
return;
}
switch(uiMode){
case MODE_HOME:
handleHomeMode();
break;
case MODE_MENU:
handleMainMenu();
break;
case MODE_TEST_CHIP:
handleTestChip_TC();
break;
case MODE_VOLT:
handleVoltimetro();
break;
case MODE_LAPTOP:
handleLaptop_TC();
break;
}
}