From 130e7a566017e536980c9057d9c6a185c3359678 Mon Sep 17 00:00:00 2001 From: pcgaldo Date: Fri, 7 Nov 2025 20:51:09 +0100 Subject: [PATCH] Enviar arquivos para "/" --- Repromod_Tester_v1_4.ino | 2937 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 2937 insertions(+) create mode 100644 Repromod_Tester_v1_4.ino diff --git a/Repromod_Tester_v1_4.ino b/Repromod_Tester_v1_4.ino new file mode 100644 index 0000000..9ac35b0 --- /dev/null +++ b/Repromod_Tester_v1_4.ino @@ -0,0 +1,2937 @@ +/* +=============================================================== +REPROMOD TESTER – v1.4 (04-11-2025 18: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 (18–21V). 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 18–21 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). +*/ +/*─────────────────────────────────────────────────────────────── + 🧾 CHANGELOG – REPROMOD TESTER FIRMWARE + ─────────────────────────────────────────────────────────────── + Base oficial: v1.0 (31-10-2025 21:47) + Hardware: Arduino MEGA 2560 Pro, INA226, OLED SSD1306 128×64 + ─────────────────────────────────────────────────────────────── + v1.4 + • Añadido mensaje “RETIRE LA BATERÍA” en modo Portátil (fase de ajuste). + • Corrección visual: flecha “ +#include +#include //V.1.11.9 +#include //V.2.5.7 +#include // ROB TILLAART V.0.4.4 +#include + +// ======================== 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; // Valor de fuente ejem. 10.5v / Valor de Ina226 ejem. 5v = V_CAL (2.10f) +// 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.4"; + +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; +static float lap_aMax = 0.0f, lap_aMin = 99.0f; +#include +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 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 + +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 &sOut); +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, + MODE_CALIB +}; +UiMode uiMode = MODE_HOME; + +enum MainMenuItem { + MM_CANAL2 = 0, + MM_TESTCHIP, + MM_MEDICION, + MM_LAPTOP, + MM_CALIBRACION, + 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 &sOut){ + 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); + + static bool blink = false; + static unsigned long blinkTs = 0; + if (millis() - blinkTs >= 500) { + blink = !blink; + blinkTs = millis(); + } + + display.setTextSize(2); + if (relayOn && blink) { + display.fillRect(0, 0, 128, 20, SSD1306_WHITE); + display.setTextColor(SSD1306_BLACK); + display.setCursor(18, 2); + display.print("ANALISIS"); + } else { + display.setTextColor(SSD1306_WHITE); + display.setCursor(18, 0); + display.print("ANALISIS"); + } + + display.setTextColor(SSD1306_WHITE); + 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(48, 52); + 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'); + extern float lap_aMax, lap_aMin; + display.setCursor(88, 35); + display.print("A Max"); + display.setCursor(88, 43); + display.print(lap_aMax, 2); + 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(98, 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(94, 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; i1)?(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 && yy999) 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,6,"CANAL 2"); + } else { + display.setCursor(4,6); + display.print("CANAL 2"); + } + + if(sel==MM_TESTCHIP){ + drawInvertedLabel(74,6,"TEST CHIP"); + } else { + display.setCursor(74,6); + display.print("TEST CHIP"); + } + + if(sel==MM_MEDICION){ + drawInvertedLabel(4,21,"VOLTIMETRO"); + } else { + display.setCursor(4,21); + display.print("VOLTIMETRO"); + } + + if(sel==MM_LAPTOP){ + drawInvertedLabel(74,21,"PORTATIL"); + } else { + display.setCursor(74,21); + display.print("PORTATIL"); + } + + if(sel==MM_CALIBRACION){ + drawInvertedLabel(4,36,"CALIBRAR"); + } else { + display.setCursor(4,36); + display.print("CALIBRAR"); + } + + drawInvertedLabel(4,52,"B1>"); + display.setCursor(95,52); + display.print("< B2"); + + 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, 30, "RANGO 18 A 21V"); + display.setTextSize(1); + display.setCursor(22,42); + display.print("RETIRE BATERIA"); + display.setCursor(4, 54); + drawInvertedLabel(4,54, "B1>"); + display.setCursor(100, 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(88, 54); + display.print("< B2"); + + display.display(); +} + +void drawCalibracionScreen(){ + float vNow=0.0f, aNow=0.0f; + readVA(vNow, aNow); + + display.clearDisplay(); + display.setTextColor(SSD1306_WHITE); + + display.setTextSize(2); + display.setCursor(17,0); + display.print("CALIBRAR"); + + display.setTextSize(1); + display.setCursor(4,20); + display.print("COLOQUE UNA R100 5W"); + display.setTextSize(1); + display.setCursor(22,30); + display.print("EN LAS PINZAS"); + display.setTextSize(1); + display.setCursor(10,40); + display.print("AJUSTE EN 10.000v"); + + drawInvertedLabel(4,52,"B1 Siguiente"); + display.setCursor(94,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(); + delaySafe(3500); + 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_aMax = 0.0f; + lap_aMin = 99.0f; + } + + if (relay1) { + if (aNow > lap_aMax) lap_aMax = aNow; + if (aNow < lap_aMin) lap_aMin = aNow; + } + + lap_lastRelayOn = relay1; + + static unsigned long lap_beepTs = 0; + if (relay1) { + if (lap_beepTs == 0) lap_beepTs = millis(); + if (millis() - lap_beepTs >= 5000UL) { + beepOnce(40); + lap_beepTs = millis(); + } + } else { + lap_beepTs = 0; + } + + 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; + } + else if(mainSel==MM_CALIBRACION){ + relay1=false; setRelay(RELAY1,false); + ch1State=CH1_IDLE; + uiMode=MODE_CALIB; + 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 (vNowvMax) 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(88,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 Siguiente"); + display.setCursor(98,52); display.print("< B2"); + display.display(); +} + +#include + +float calI_k = 1.000f; +float calV_k = 1.000f; +const uint16_t EE_CAL_I_ADDR = 96; +const uint16_t EE_CAL_V_ADDR = 100; + +void saveCalNV(){ EEPROM.put(EE_CAL_I_ADDR, calI_k); EEPROM.put(EE_CAL_V_ADDR, calV_k); } +void loadCalNV(){ EEPROM.get(EE_CAL_I_ADDR, calI_k); EEPROM.get(EE_CAL_V_ADDR, calV_k); if(!(calI_k>0.5f && calI_k<1.5f)) calI_k=1.0f; +if(!(calV_k>0.5f && calV_k<1.5f)) calV_k=1.0f; } + +void drawCalibStep2(float iDisp, float kI){ + display.clearDisplay(); + display.setTextColor(SSD1306_WHITE); + display.setTextSize(2); display.setCursor(17,0); display.print("CALIBRAR"); + display.setTextSize(1); display.setCursor(10,18); display.print("AJUSTE A 0.100 AMP"); + display.setTextSize(2);display.setCursor(28,28); display.print(iDisp,3); display.print("A"); + drawInvertedLabel(35,48,"B1+ B2-"); + display.display(); +} + +void drawCalibStep3(float vDisp, float kV){ + display.clearDisplay(); + display.setTextColor(SSD1306_WHITE); + display.setTextSize(2); display.setCursor(17,0); display.print("CALIBRAR"); + display.setTextSize(1); display.setCursor(10,18); display.print("AJUSTE A 10.00 VOL"); + display.setTextSize(2);display.setCursor(28,28); display.print(vDisp,2); display.print("V"); + drawInvertedLabel(35,48,"B1+ B2-"); + display.display(); +} + +void drawCalibDone(){ + display.clearDisplay(); + display.setTextColor(SSD1306_WHITE); + display.setTextSize(2); display.setCursor(6,0); display.print("COMPLETADO"); + display.setTextSize(1); display.setCursor(12,24); display.print("CAL_I "); display.print(calI_k,3); + display.setCursor(12,36); display.print("CAL_V "); display.print(calV_k,3); + display.display(); +} + +void handleCalibracion(){ + static uint8_t calPhase = 0; + static unsigned long calI_okStart = 0, calV_okStart = 0; + static unsigned long calI_outTs = 0, calV_outTs = 0; + static float iFilt = 0.0f, vFilt = 0.0f; +if (calPhase==0 && consumeClick(BTN2)) { + beepOnce(40); + relay1 = false; setRelay(RELAY1, false); + uiMode = MODE_MENU; + registerActivity(); + return; +} + if(calPhase==0){ + if(consumeClick(BTN1)){ + beepOnce(80); + relay1=true; setRelay(RELAY1,true); + 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>=9.5f && vAvg<=10.5f){ calPhase=1; registerActivity(); } + else{ + display.clearDisplay(); + display.setTextColor(SSD1306_WHITE); + display.setTextSize(2); display.setCursor(18,0); display.print("ATENCION"); + display.setTextSize(1); display.setCursor(8,24); display.print("Medido: "); display.print(vAvg,2); display.print("V"); + drawInvertedLabel(6,40,"DESAJUSTE EXCESIVO"); + drawInvertedLabel(6,50,"AJUSTE V_CAL ANTES"); + display.display(); + delaySafe(10000); + } + } + drawCalibStep1(); + return; + } + +if(calPhase==1){ + if(!relay1){ relay1=true; setRelay(RELAY1,true); } + float vNow=0.0f, aNow=0.0f; readVA(vNow,aNow); + iFilt += (aNow - iFilt) * 0.30f; + if(consumeClick(BTN1)){ float v=calI_k+0.01f; if(v>1.20f) v=1.20f; calI_k=v; beepOnce(40); registerActivity(); } + if(consumeClick(BTN2)){ float v=calI_k-0.01f; if(v<0.80f) v=0.80f; calI_k=v; beepOnce(40); registerActivity(); } + float iDisp = iFilt*calI_k; + bool inRange = (iDisp>=0.095f && iDisp<=0.105f); + unsigned long now = millis(); + if(inRange){ + if(calI_okStart==0) calI_okStart = now; + calI_outTs = 0; + }else{ + if(calI_outTs==0) calI_outTs = now; + if(now - calI_outTs >= 150UL){ calI_okStart = 0; } + } + drawCalibStep2(iDisp, calI_k); + if(calI_okStart){ + unsigned long dt = now - calI_okStart; if(dt>5000UL) dt=5000UL; + uint16_t w = (uint16_t)(dt*128UL/5000UL); + display.fillRect(0,62,w,2,SSD1306_WHITE); + display.display(); + if(dt>=5000UL){ calPhase=2; registerActivity(); } + }else{ + display.drawRect(0,62,128,2,SSD1306_WHITE); + display.display(); + } + return; +} + +if(calPhase==2){ + float vNow=0.0f, aNow=0.0f; readVA(vNow,aNow); + if(consumeClick(BTN1)){ float v=calV_k+0.01f; if(v>1.20f) v=1.20f; calV_k=v; beepOnce(40); registerActivity(); } + if(consumeClick(BTN2)){ float v=calV_k-0.01f; if(v<0.80f) v=0.80f; calV_k=v; beepOnce(40); registerActivity(); } + float vDisp = vNow*calV_k; + if(vDisp>=9.90f && vDisp<=10.10f){ if(calV_okStart==0) calV_okStart=millis(); } + else { calV_okStart=0; } + drawCalibStep3(vDisp, calV_k); + if(calV_okStart){ + unsigned long dt = millis()-calV_okStart; if(dt>5000UL) dt=5000UL; + uint16_t w = (uint16_t)(dt*128UL/5000UL); + display.fillRect(0,62,w,2,SSD1306_WHITE); + display.display(); + if(dt>=5000UL){ saveCalNV(); calPhase=3; calV_okStart=0; registerActivity(); } + } else { + display.drawRect(0,62,128,2,SSD1306_WHITE); + display.display(); + } + return; +} + + if(calPhase>=3){ + relay1=false; setRelay(RELAY1,false); + drawCalibDone(); + delaySafe(1200); + uiMode = MODE_MENU; + calPhase = 0; + return; + } +} + + +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; + + case MODE_CALIB: + handleCalibracion(); + break; + } +} +