/* =============================================================== 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 (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). */ #include #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; // 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 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 }; 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 &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); 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; 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,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 (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(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