repromod-tester/Repromod_Tester_v1.ino

2694 lines
62 KiB
Arduino
Raw Normal View History

2025-11-03 17:25:49 +01:00
/*
===============================================================
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;
}
}