1042 lines
40 KiB
Arduino
1042 lines
40 KiB
Arduino
#include <WiFi.h>
|
||
#include <WebServer.h>
|
||
#include <WebSocketsServer.h>
|
||
#include <Update.h>
|
||
#include <time.h>
|
||
#include <TMC2209.h>
|
||
#include <FlexyStepper.h>
|
||
#include <Wire.h>
|
||
#include <SparkFun_ADS122C04_ADC_Arduino_Library.h>
|
||
#include <math.h>
|
||
|
||
#define VERBO 0
|
||
|
||
// -------------------- Declaraciones que ya usas --------------------
|
||
void externalADC_init();
|
||
void setXY();
|
||
void trackSunByTime();
|
||
void nomegusta(int32_t varerr, int32_t error);
|
||
void leetodo();
|
||
int32_t current(volatile uint32_t data);
|
||
|
||
// --- Auto-Tune ---
|
||
void at_recommend_thresholds(bool applyNow);
|
||
void handleRecomputeAutoTune();
|
||
void handleApplyAutoTune();
|
||
void handleSetAutoApply();
|
||
|
||
// -------------------- WiFi --------------------
|
||
// Local configuration; see wifi_credentials.example.h.
|
||
#include "wifi_credentials.h"
|
||
|
||
// -------------------- NTP (time.h) --------------------
|
||
const char* ntpServer = "pool.ntp.org";
|
||
// Aguascalientes típico: UTC-6 sin DST (ajústalo si quieres DST)
|
||
const long gmtOffset_sec = -6 * 3600;
|
||
const int daylightOffset_sec = 0;
|
||
|
||
// -------------------- Web --------------------
|
||
WebServer server(80);
|
||
WebSocketsServer webSocket = WebSocketsServer(81);
|
||
|
||
// -------------------- Log --------------------
|
||
#define MAX_LOG_SIZE 100
|
||
String logBuffer[MAX_LOG_SIZE];
|
||
int logIndex = 0;
|
||
|
||
// -------------------- Motores / ADC (tu base) --------------------
|
||
volatile int adcDataAvailable = 0;
|
||
volatile uint32_t adch[3];
|
||
volatile bool VocIsc = 0;
|
||
|
||
volatile float SmoothData, SmoothData2, SmoothVolt;
|
||
volatile float LPF_Beta = 0.85;
|
||
|
||
SFE_ADS122C04 mySensor;
|
||
HardwareSerial serial_stream = Serial0;
|
||
|
||
const int MOTOR_X_STEP_PIN = 3;
|
||
const int MOTOR_X_DIRECTION_PIN = 2;
|
||
const int MOTOR_Y_STEP_PIN = 13;
|
||
const int MOTOR_Y_DIRECTION_PIN = 12;
|
||
|
||
const long SERIAL_BAUD_RATE = 250000;
|
||
const uint8_t REPLY_DELAY = 15;
|
||
|
||
const uint8_t RUN_CURRENT_PERCENT0 = 18;
|
||
const int32_t VELOCITY0 = 4200;
|
||
const uint8_t STALL_GUARD_THRESHOLD0 = 200;
|
||
|
||
const uint8_t RUN_CURRENT_PERCENT1 = 25;
|
||
const int32_t VELOCITY1 = 4200;
|
||
const uint8_t STALL_GUARD_THRESHOLD1 = 200;
|
||
|
||
FlexyStepper stepperX;
|
||
FlexyStepper stepperY;
|
||
|
||
TMC2209 stepper_driver_0;
|
||
const TMC2209::SerialAddress SERIAL_ADDRESS_1 = TMC2209::SERIAL_ADDRESS_1;
|
||
TMC2209 stepper_driver_1;
|
||
const TMC2209::SerialAddress SERIAL_ADDRESS_3 = TMC2209::SERIAL_ADDRESS_3;
|
||
|
||
const int32_t LIMITX = -104000;
|
||
const int32_t LIMITY = -106000;
|
||
const int32_t hist = 100;
|
||
|
||
const int32_t extrafino = 30;
|
||
const int32_t fino = 80;
|
||
const int32_t burdo = 200;
|
||
|
||
volatile int32_t cicleMAX = 3;
|
||
|
||
volatile int32_t cicles = 0;
|
||
volatile int32_t errmax = 500;
|
||
volatile int32_t delayyy = 3;
|
||
|
||
volatile int32_t min_errmax = 30;
|
||
volatile int32_t abrupt_threshold = 200;
|
||
volatile int32_t small_threshold = 20;
|
||
volatile int32_t reset_threshold = 500;
|
||
volatile float peak_proximity = 0.05;
|
||
|
||
volatile int32_t posX = 0;
|
||
volatile int32_t posY = 0;
|
||
|
||
volatile int8_t dirX = 1;
|
||
volatile int8_t dirY = 1;
|
||
|
||
volatile bool Xactive = 1;
|
||
|
||
volatile uint16_t status = 0;
|
||
|
||
uint8_t contador = 0;
|
||
volatile uint32_t rawval[4];
|
||
|
||
volatile int32_t adc_old = 1000;
|
||
|
||
volatile uint32_t newADC;
|
||
volatile int32_t adc, error, pira, volt;
|
||
volatile uint32_t tatras = 0;
|
||
volatile uint32_t elapsed = 0;
|
||
|
||
// -------------------- Solar (Aguascalientes) --------------------
|
||
const float LATITUDE = 21.88; // °N
|
||
const float LONGITUDE = -102.29; // °E negativo Oeste
|
||
const float TILT_ANGLE = 20.0; // inclinación mecánica hacia el sur
|
||
|
||
// ==================== AUTO-TUNE BUFFER ====================
|
||
#ifndef AT_CAP
|
||
#define AT_CAP 180 // ~3 min a 1 Hz
|
||
#endif
|
||
|
||
struct AutoTuneBuf {
|
||
int32_t C[AT_CAP]; // Isc (cuentas ADC de AIN0/1)
|
||
int32_t I[AT_CAP]; // Irradiancia W/m2 (de V2 ajustada)
|
||
uint32_t t[AT_CAP]; // millis
|
||
int head = -1;
|
||
int count = 0;
|
||
} atb;
|
||
|
||
static inline int at_index(int k) { return (k + AT_CAP) % AT_CAP; }
|
||
|
||
void at_push(int32_t Cval, int32_t Ival, uint32_t now_ms) {
|
||
atb.head = (atb.head + 1) % AT_CAP;
|
||
atb.C[atb.head] = Cval;
|
||
atb.I[atb.head] = Ival;
|
||
atb.t[atb.head] = now_ms;
|
||
if (atb.count < AT_CAP) atb.count++;
|
||
}
|
||
|
||
template<typename T>
|
||
float quantile_copy(const T* arr, int n, float q) {
|
||
static T tmp[AT_CAP];
|
||
if (n <= 0) return 0;
|
||
for (int i = 0; i < n; i++) tmp[i] = arr[i];
|
||
for (int i = 1; i < n; i++) { // insertion sort
|
||
T key = tmp[i]; int j = i - 1;
|
||
while (j >= 0 && tmp[j] > key) { tmp[j+1] = tmp[j]; j--; }
|
||
tmp[j+1] = key;
|
||
}
|
||
float idx = (n - 1) * q;
|
||
int i0 = (int)idx;
|
||
int i1 = min(i0 + 1, n - 1);
|
||
float frac = idx - i0;
|
||
return tmp[i0] * (1 - frac) + tmp[i1] * frac;
|
||
}
|
||
|
||
volatile bool at_auto_apply = false;
|
||
unsigned long at_last_apply_ms = 0;
|
||
const unsigned long AT_PERIOD_MS = 60000UL;
|
||
|
||
// ==================== UTIL DE LOG ====================
|
||
String getTimeStamp() {
|
||
struct tm timeinfo;
|
||
if (!getLocalTime(&timeinfo)) return "[sin hora]";
|
||
char buffer[25];
|
||
strftime(buffer, sizeof(buffer), "[%Y-%m-%d %H:%M:%S]", &timeinfo);
|
||
return String(buffer);
|
||
}
|
||
|
||
void agregarLog(String texto) {
|
||
String entrada = getTimeStamp() + " " + texto;
|
||
Serial.println(entrada);
|
||
logBuffer[logIndex] = entrada;
|
||
logIndex = (logIndex + 1) % MAX_LOG_SIZE;
|
||
webSocket.broadcastTXT("LOG:" + entrada);
|
||
}
|
||
|
||
void limpiarLog() {
|
||
for (int i = 0; i < MAX_LOG_SIZE; i++) logBuffer[i] = "";
|
||
logIndex = 0;
|
||
webSocket.broadcastTXT("LOG:[LOG LIMPIADO]");
|
||
Serial.println("[LOG LIMPIADO]");
|
||
}
|
||
|
||
void sendPositionUpdate() {
|
||
String posMsg = "POS:" + String(-posX) + ":" + String(-posY);
|
||
webSocket.broadcastTXT(posMsg);
|
||
}
|
||
|
||
// ==================== WEBSOCKET ====================
|
||
void iniciarWebSocket() {
|
||
webSocket.begin();
|
||
webSocket.onEvent([](uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
|
||
if (type == WStype_CONNECTED) {
|
||
for (int i = 0; i < MAX_LOG_SIZE; i++) {
|
||
int idx = (logIndex + i) % MAX_LOG_SIZE;
|
||
if (logBuffer[idx].length() > 0) webSocket.sendTXT(num, "LOG:" + logBuffer[idx]);
|
||
}
|
||
String posMsg = "POS:" + String(-posX) + ":" + String(-posY);
|
||
webSocket.sendTXT(num, posMsg);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==================== ADC EXTERNO ====================
|
||
void externalADC_init() {
|
||
Wire.begin();
|
||
mySensor.begin(0x40, Wire);
|
||
mySensor.setInputMultiplexer(ADS122C04_MUX_AIN2_AVSS);
|
||
mySensor.setGain(ADS122C04_GAIN_1);
|
||
mySensor.setOperatingMode(ADS122C04_OP_MODE_NORMAL);
|
||
mySensor.setConversionMode(ADS122C04_CONVERSION_MODE_SINGLE_SHOT);
|
||
mySensor.setVoltageReference(ADS122C04_VREF_INTERNAL);
|
||
mySensor.setDataCounter(ADS122C04_DCNT_DISABLE);
|
||
mySensor.setDataIntegrityCheck(ADS122C04_CRC_DISABLED);
|
||
mySensor.setBurnOutCurrent(ADS122C04_BURN_OUT_CURRENT_OFF);
|
||
mySensor.setIDACcurrent(ADS122C04_IDAC_CURRENT_OFF);
|
||
mySensor.setIDAC1mux(ADS122C04_IDAC1_DISABLED);
|
||
mySensor.setIDAC2mux(ADS122C04_IDAC2_DISABLED);
|
||
}
|
||
|
||
// ==================== LECTURA ADC ====================
|
||
// AIN0/1: Isc (corto). AIN2: piranómetro. AIN3: Voc.
|
||
// Se conmuta con pin 9: HIGH => SC, LOW => Voc.
|
||
void leetodo() {
|
||
for (contador = 0; contador < 4; contador++) {
|
||
if (contador == 0) { // Isc (cortocircuito vía conmutación)
|
||
mySensor.setInputMultiplexer(ADS122C04_MUX_AIN1_AVSS);
|
||
digitalWrite(9, HIGH);
|
||
}
|
||
if (contador == 1) // AIN0 (relacionado con Isc)
|
||
mySensor.setInputMultiplexer(ADS122C04_MUX_AIN0_AVSS);
|
||
if (contador == 2) { // Piranómetro
|
||
mySensor.setInputMultiplexer(ADS122C04_MUX_AIN2_AVSS);
|
||
digitalWrite(9, LOW);
|
||
}
|
||
if (contador == 3) { // Voc
|
||
mySensor.setInputMultiplexer(ADS122C04_MUX_AIN3_AVSS);
|
||
}
|
||
|
||
mySensor.start();
|
||
unsigned long start_time = millis();
|
||
boolean drdy = false;
|
||
while (!drdy && (millis() < (start_time + ADS122C04_CONVERSION_TIMEOUT))) {
|
||
delay(5);
|
||
drdy = mySensor.checkDataReady();
|
||
}
|
||
if (!drdy) { Serial.println(F("checkDataReady timed out")); return; }
|
||
|
||
rawval[contador] = mySensor.readADC();
|
||
|
||
// Diagnóstico V0..V3 (ajustes: V2 ÷2 para W/m2; V3 ×2 por divisor)
|
||
Serial.print(F(",V"));
|
||
Serial.print(contador);
|
||
Serial.print(":");
|
||
float base = ((float)rawval[contador] / 8388607.0f) * 2048.0f; // V
|
||
if (contador == 2) { // Piranómetro → W/m2 directo
|
||
Serial.print(base / 2.0f);
|
||
} else if (contador == 3) { // Voc (divisor 1/2)
|
||
Serial.print(base * 2.0f);
|
||
} else {
|
||
Serial.print(base);
|
||
}
|
||
delay(50);
|
||
}
|
||
}
|
||
|
||
// ==================== CORRIENTE ====================
|
||
int32_t current(volatile uint32_t data) { return (int32_t)data; }
|
||
|
||
// ==================== FINE-TUNE (tu base con dynamic err) ====================
|
||
void nomegusta(int32_t value, int32_t e) {
|
||
int32_t current_change = abs(value - adc_old);
|
||
|
||
static int32_t dynamic_errmax = errmax;
|
||
static int32_t max_current = -value;
|
||
static bool near_peak = false;
|
||
static uint8_t escape_counter = 0;
|
||
|
||
if (-value > max_current) {
|
||
max_current = -value;
|
||
near_peak = true;
|
||
dynamic_errmax = min_errmax;
|
||
e = dynamic_errmax;
|
||
escape_counter = 0;
|
||
} else if (-value >= max_current * (1.0 - peak_proximity)) {
|
||
near_peak = true;
|
||
dynamic_errmax = min_errmax;
|
||
e = dynamic_errmax / (cicles + 1);
|
||
if (e < min_errmax) e = min_errmax;
|
||
escape_counter++;
|
||
} else if (current_change > abrupt_threshold && cicles > 0) {
|
||
near_peak = false;
|
||
dynamic_errmax = (dynamic_errmax * 60) / 100;
|
||
if (dynamic_errmax < min_errmax) dynamic_errmax = min_errmax;
|
||
e = dynamic_errmax;
|
||
escape_counter = 0;
|
||
} else if (current_change < small_threshold && cicles > 0) {
|
||
char log_buff[120];
|
||
snprintf(log_buff, sizeof(log_buff),
|
||
"FineTune: dC=%d, dynE=%d, e=%d, near=%d, Cmax=%d",
|
||
current_change, dynamic_errmax, e, near_peak, max_current);
|
||
agregarLog(log_buff);
|
||
sendPositionUpdate();
|
||
escape_counter++;
|
||
return;
|
||
} else if (current_change > reset_threshold) {
|
||
near_peak = false;
|
||
dynamic_errmax = errmax;
|
||
max_current = -value;
|
||
e = dynamic_errmax;
|
||
escape_counter = 0;
|
||
} else {
|
||
e = dynamic_errmax;
|
||
escape_counter++;
|
||
}
|
||
|
||
if (escape_counter >= 10) {
|
||
near_peak = false;
|
||
dynamic_errmax = errmax;
|
||
e = dynamic_errmax;
|
||
escape_counter = 0;
|
||
}
|
||
|
||
char log_buff[120];
|
||
snprintf(log_buff, sizeof(log_buff),
|
||
"FineTune: dC=%d, dynE=%d, e=%d, near=%d, Cmax=%d",
|
||
current_change, dynamic_errmax, e, near_peak, max_current);
|
||
agregarLog(log_buff);
|
||
|
||
if (value < adc_old || cicles == 0) {
|
||
if (cicles == 0 && value > adc_old) { if (Xactive) dirY *= -1; else dirX *= -1; }
|
||
if (Xactive) {
|
||
posX += dirX * e;
|
||
if (posX >= 0) posX = 0;
|
||
if (posX <= LIMITX) posX = LIMITX;
|
||
stepperX.moveToPositionInSteps(posX);
|
||
} else {
|
||
posY += dirY * e;
|
||
if (posY >= 0) posY = 0;
|
||
if (posY <= LIMITY) posY = LIMITY;
|
||
stepperY.moveToPositionInSteps(posY);
|
||
}
|
||
} else {
|
||
if (Xactive) {
|
||
dirX *= -1;
|
||
posX += dirX * e;
|
||
if (posX >= 0) posX = 0;
|
||
if (posX <= LIMITX) posX = LIMITX;
|
||
stepperX.moveToPositionInSteps(posX);
|
||
} else {
|
||
dirY *= -1;
|
||
posY += dirY * e;
|
||
if (posY >= 0) posY = 0;
|
||
if (posY <= LIMITY) posY = LIMITY;
|
||
stepperY.moveToPositionInSteps(posY);
|
||
}
|
||
}
|
||
|
||
bool moving = true;
|
||
while (moving) {
|
||
stepperX.processMovement();
|
||
stepperY.processMovement();
|
||
moving = !stepperX.motionComplete() || !stepperY.motionComplete();
|
||
delay(1);
|
||
}
|
||
|
||
adc_old = value;
|
||
cicles++;
|
||
if (cicles >= cicleMAX) {
|
||
cicles = 0;
|
||
Xactive = !Xactive; // alterna eje
|
||
dynamic_errmax = errmax; // reset por eje
|
||
max_current = -value;
|
||
near_peak = false;
|
||
escape_counter = 0;
|
||
}
|
||
|
||
sendPositionUpdate();
|
||
}
|
||
|
||
// ==================== TRACK SUN BY TIME (tu versión) ====================
|
||
void trackSunByTime() {
|
||
status = 3;
|
||
|
||
struct tm timeinfo;
|
||
if (!getLocalTime(&timeinfo)) { agregarLog("Error al obtener hora NTP"); status = 0; return; }
|
||
|
||
char time_buff[120];
|
||
snprintf(time_buff, sizeof(time_buff),
|
||
"Timeinfo: year=%d, mon=%d, day=%d, hour=%d, min=%d, sec=%d",
|
||
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
||
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
||
agregarLog(time_buff);
|
||
|
||
int year = timeinfo.tm_year + 1900;
|
||
int month = timeinfo.tm_mon + 1;
|
||
int day = timeinfo.tm_mday;
|
||
int hour = timeinfo.tm_hour;
|
||
int minute = timeinfo.tm_min;
|
||
int second = timeinfo.tm_sec;
|
||
|
||
int a = (14 - month) / 12;
|
||
int y = year + 4800 - a;
|
||
int m = month + 12 * a - 3;
|
||
double jd0 = day + floor((153 * m + 2) / 5.0) + 365.0 * y + floor(y / 4.0) - floor(y / 100.0) + floor(y / 400.0) - 32045.5;
|
||
// local→UTC (ajusta si usas DST manual)
|
||
double ut_hour = hour + minute / 60.0 + second / 3600.0 + 6.0;
|
||
double jdn = jd0 + ut_hour / 24.0;
|
||
|
||
char ut_buff[60];
|
||
snprintf(ut_buff, sizeof(ut_buff), "UT_hour: %.6f", ut_hour);
|
||
agregarLog(ut_buff);
|
||
|
||
double t = (jdn - 2451545.0) / 36525.0;
|
||
double mean_anomaly = 357.5291 + 35999.0503 * t - 0.0001537 * t * t;
|
||
mean_anomaly = fmod(mean_anomaly, 360.0) * M_PI / 180.0;
|
||
double C = 1.914602 * sin(mean_anomaly) + 0.019993 * sin(2.0 * mean_anomaly) + 0.000289 * sin(3.0 * mean_anomaly);
|
||
double L = 280.46646 + 36000.76983 * t + 0.0003032 * t * t; L = fmod(L, 360.0) * M_PI / 180.0;
|
||
double ecliptic_long = L + C * M_PI / 180.0;
|
||
double epsilon = (23.4392911 - 0.0130042 * t - 0.000000163 * t * t + 0.0000005036 * t * t * t) * M_PI / 180.0;
|
||
|
||
double declination = asin(sin(epsilon) * sin(ecliptic_long));
|
||
double ra = atan2(cos(epsilon) * sin(ecliptic_long), cos(ecliptic_long)); if (ra < 0) ra += 2 * M_PI;
|
||
|
||
double D = jdn - 2451545.0;
|
||
double gmst_hours = 6.697374558 + 0.06570982441908 * D + ut_hour * 1.00273790935;
|
||
gmst_hours = fmod(gmst_hours, 24.0); if (gmst_hours < 0) gmst_hours += 24.0;
|
||
double gmst = gmst_hours * 15.0 * M_PI / 180.0;
|
||
|
||
double lon_rad = LONGITUDE * M_PI / 180.0;
|
||
double lst = gmst + lon_rad; lst = fmod(lst, 2.0 * M_PI); if (lst < 0) lst += 2.0 * M_PI;
|
||
|
||
double ha = lst - ra; if (ha < -M_PI) ha += 2 * M_PI; if (ha > M_PI) ha -= 2 * M_PI;
|
||
|
||
double phi = LATITUDE * M_PI / 180.0;
|
||
|
||
double sin_alt = sin(phi) * sin(declination) + cos(phi) * cos(declination) * cos(ha);
|
||
sin_alt = constrain(sin_alt, -1.0, 1.0);
|
||
|
||
double altitude_rad = asin(sin_alt);
|
||
double altitude = altitude_rad * 180.0 / M_PI;
|
||
double zenith_raw = 90.0 - altitude;
|
||
|
||
double cos_alt = cos(altitude_rad);
|
||
if (fabs(cos_alt) < 1e-12) cos_alt = (cos_alt >= 0) ? 1e-12 : -1e-12;
|
||
|
||
double sin_az = -cos(declination) * sin(ha) / cos_alt;
|
||
double cos_az = (sin(declination) - sin(phi) * sin_alt) / (cos(phi) * cos_alt);
|
||
double azimuth = atan2(sin_az, cos_az) * 180.0 / M_PI; if (azimuth < 0) azimuth += 360.0;
|
||
|
||
double zenith = zenith_raw - TILT_ANGLE;
|
||
if (zenith < 0.0) zenith = 0.0;
|
||
|
||
char debug_buff[200];
|
||
snprintf(debug_buff, sizeof(debug_buff),
|
||
"Debug: jdn=%.3f, t=%.6f, ra=%.3f, ha=%.3f, decl=%.3f, sin_alt=%.3f, gmst=%.3f, lst=%.3f, alt=%.3f",
|
||
jdn, t, ra * 180.0 / M_PI, ha * 180.0 / M_PI, declination * 180.0 / M_PI,
|
||
sin_alt, gmst * 180.0 / M_PI, lst * 180.0 / M_PI, altitude);
|
||
agregarLog(debug_buff);
|
||
|
||
// Map azimuth → Y
|
||
double az_offset = azimuth - 180.0;
|
||
az_offset = constrain(az_offset, -80.0, 80.0);
|
||
int32_t target_posY = -(int32_t)(53000.0 + az_offset * 106000.0 / 160.0);
|
||
|
||
// Map zenith → X
|
||
if (zenith > 50.0) zenith = 50.0;
|
||
int32_t target_posX = -(int32_t)(zenith * 104000.0 / 50.0);
|
||
|
||
posX = target_posX; posY = target_posY;
|
||
if (posX >= 0) posX = 0; if (posX <= LIMITX) posX = LIMITX;
|
||
if (posY >= 0) posY = 0; if (posY <= LIMITY) posY = LIMITY;
|
||
stepperX.moveToPositionInSteps(posX);
|
||
stepperY.moveToPositionInSteps(posY);
|
||
|
||
char buff[120];
|
||
snprintf(buff, sizeof(buff), "SunPos: Az=%.1f, Zen=%.1f (raw=%.1f), posX=%d, posY=%d",
|
||
azimuth, zenith, zenith_raw, posX, posY);
|
||
agregarLog(buff);
|
||
|
||
bool moving = true;
|
||
while (moving) {
|
||
stepperX.processMovement();
|
||
stepperY.processMovement();
|
||
moving = !stepperX.motionComplete() || !stepperY.motionComplete();
|
||
delay(1);
|
||
}
|
||
|
||
int32_t currentValue = current(rawval[1]);
|
||
nomegusta(-currentValue, errmax);
|
||
sendPositionUpdate();
|
||
}
|
||
|
||
// ==================== HOMING ====================
|
||
void setXY() {
|
||
stepper_driver_0.setMicrostepsPerStepPowerOfTwo(3);
|
||
long time = millis();
|
||
uint16_t contador = 0;
|
||
uint32_t s = stepper_driver_0.getStallGuardResult();
|
||
uint32_t resultado = 500;
|
||
|
||
stepper_driver_0.moveAtVelocity(VELOCITY0);
|
||
stepper_driver_0.disableInverseMotorDirection();
|
||
delay(100);
|
||
|
||
while (millis() - time < 45500 && resultado > 180) {
|
||
s += stepper_driver_0.getStallGuardResult();
|
||
contador++;
|
||
if (contador == 272) {
|
||
resultado = s / 272; s = 0; contador = 0;
|
||
#ifdef VERBO
|
||
Serial.print("stall_guard_result = "); Serial.println(resultado);
|
||
#endif
|
||
}
|
||
}
|
||
stepper_driver_0.moveAtVelocity(0);
|
||
stepper_driver_0.setStandstillMode(stepper_driver_0.NORMAL);
|
||
|
||
stepper_driver_1.moveAtVelocity(VELOCITY1);
|
||
stepper_driver_1.enableInverseMotorDirection();
|
||
delay(100);
|
||
|
||
time = millis(); contador = 0;
|
||
s = stepper_driver_1.getStallGuardResult(); resultado = 500;
|
||
while (millis() - time < 45000 && resultado > 185) {
|
||
s += stepper_driver_1.getStallGuardResult();
|
||
contador++;
|
||
if (contador == 272) {
|
||
resultado = s / 272; s = 0; contador = 0;
|
||
#ifdef VERBO
|
||
Serial.print("stall_guard_motorY = "); Serial.println(resultado);
|
||
#endif
|
||
}
|
||
}
|
||
#ifdef VERBO
|
||
Serial.print("stall_guard_result-- = "); Serial.println(resultado);
|
||
Serial.println("MOTOR Y SET");
|
||
#endif
|
||
stepper_driver_1.moveAtVelocity(0);
|
||
stepper_driver_1.setStandstillMode(stepper_driver_1.NORMAL);
|
||
|
||
stepperX.setCurrentPositionInSteps(0);
|
||
stepperY.setCurrentPositionInSteps(0);
|
||
|
||
stepper_driver_0.setMicrostepsPerStepPowerOfTwo(3);
|
||
stepper_driver_1.setMicrostepsPerStepPowerOfTwo(3);
|
||
|
||
stepperX.setSpeedInStepsPerSecond(6200);
|
||
stepperX.setAccelerationInStepsPerSecondPerSecond(9000);
|
||
stepperY.setSpeedInStepsPerSecond(6200);
|
||
stepperY.setAccelerationInStepsPerSecondPerSecond(9000);
|
||
|
||
posX = 0; posY = 0;
|
||
sendPositionUpdate();
|
||
}
|
||
|
||
// ==================== PÁGINA WEB ====================
|
||
void handleRoot() {
|
||
server.sendHeader("Content-Type", "text/html; charset=UTF-8");
|
||
String html = R"rawliteral(
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>ESP32 Logger & Stepper Control</title>
|
||
<style>
|
||
.container { display:flex; flex-wrap:wrap; gap:20px; margin-top:20px; }
|
||
.column { flex:1; min-width:300px; max-width:45%; }
|
||
@media (max-width:800px){ .column{ max-width:100%; } }
|
||
.param-group { margin-bottom:10px; }
|
||
button{ margin:2px 0; }
|
||
</style>
|
||
<script>
|
||
var ws = new WebSocket("ws://" + location.hostname + ":81/");
|
||
ws.onmessage = function(event){
|
||
var data = event.data;
|
||
if (data.startsWith("LOG:")) {
|
||
var log = document.getElementById("log");
|
||
log.innerHTML += data.substring(4) + "<br>";
|
||
log.scrollTop = log.scrollHeight;
|
||
} else if (data.startsWith("POS:")) {
|
||
var parts = data.split(":");
|
||
if (parts.length === 3) {
|
||
document.getElementById("posXDisplay").innerText = parts[2];
|
||
document.getElementById("posYDisplay").innerText = parts[1];
|
||
updatePlot(parts[2], parts[1]);
|
||
}
|
||
}
|
||
};
|
||
function updatePlot(x, y){
|
||
var canvas = document.getElementById("positionPlot");
|
||
var ctx = canvas.getContext("2d");
|
||
var w = canvas.width, h = canvas.height, m=40, maxX=106000, maxY=104000;
|
||
ctx.clearRect(0,0,w,h);
|
||
ctx.strokeStyle="#ccc"; ctx.lineWidth=1;
|
||
for (var i=0;i<=10;i++){
|
||
var xPos=m+(i*(w-2*m)/10);
|
||
ctx.beginPath(); ctx.moveTo(xPos,m); ctx.lineTo(xPos,h-m); ctx.stroke();
|
||
ctx.fillStyle="#000"; ctx.font="12px Arial"; ctx.fillText(Math.round(i*maxX/10), xPos-10, h-m+15);
|
||
var yPos=m+(i*(h-2*m)/10);
|
||
ctx.beginPath(); ctx.moveTo(m,yPos); ctx.lineTo(w-m,yPos); ctx.stroke();
|
||
ctx.fillText(Math.round((10-i)*maxY/10), m-30, yPos+5);
|
||
}
|
||
ctx.strokeStyle="#000"; ctx.lineWidth=2;
|
||
ctx.beginPath(); ctx.moveTo(m,m); ctx.lineTo(m,h-m); ctx.lineTo(w-m,h-m); ctx.stroke();
|
||
var plotX=m+(x/maxX)*(w-2*m);
|
||
var plotY=h-m-(y/maxY)*(h-2*m);
|
||
ctx.fillStyle="red"; ctx.beginPath(); ctx.arc(plotX,plotY,5,0,2*Math.PI); ctx.fill();
|
||
}
|
||
function sendCommand(s){ fetch('/command?status='+s).catch(console.error); }
|
||
function setCoords(){
|
||
var x=document.getElementById("posX").value;
|
||
var y=document.getElementById("posY").value;
|
||
fetch('/setCoords?x='+y+'&y='+x).catch(console.error);
|
||
}
|
||
// NEW: Set Current Position from inputs (sin homing)
|
||
function setCurrentPos(){
|
||
var x=document.getElementById("posX").value;
|
||
var y=document.getElementById("posY").value;
|
||
if(x===""||y===""){ alert('Fill X and Y'); return; }
|
||
fetch('/command?status=6&x='+y+'&y='+x).catch(console.error);
|
||
}
|
||
function setLPFBeta(){ var v=document.getElementById("lpfBeta").value; fetch('/setLPFBeta?beta='+v); }
|
||
function setErrMax(){ var v=document.getElementById("errMax").value; fetch('/setErrMax?err='+v); }
|
||
function setDelay(){ var v=document.getElementById("delayyy").value; fetch('/setDelay?delay='+v); }
|
||
function setMinErrMax(){ var v=document.getElementById("minErrMax").value; fetch('/setMinErrMax?minErr='+v); }
|
||
function setAbruptThreshold(){ var v=document.getElementById("abruptThreshold").value; fetch('/setAbruptThreshold?abrupt='+v); }
|
||
function setSmallThreshold(){ var v=document.getElementById("smallThreshold").value; fetch('/setSmallThreshold?small='+v); }
|
||
function setResetThreshold(){ var v=document.getElementById("resetThreshold").value; fetch('/setResetThreshold?reset='+v); }
|
||
function setPeakProximity(){ var v=document.getElementById("peakProximity").value; fetch('/setPeakProximity?peak='+v); }
|
||
function setCicleMax(){ var v=document.getElementById("cicleMax").value; fetch('/setCicleMax?cicle='+v); }
|
||
|
||
// Auto-Tune UI
|
||
function toggleAutoApply(){ var ck=document.getElementById("autoApply").checked ? 1 : 0; fetch('/setAutoApply?on='+ck); }
|
||
function recomputeAT(){ fetch('/recomputeAutoTune'); }
|
||
function applyAT(){ fetch('/applyAutoTune'); }
|
||
|
||
window.onload=function(){
|
||
document.getElementById("lpfBeta").value="0.85";
|
||
document.getElementById("errMax").value="500";
|
||
document.getElementById("delayyy").value="3";
|
||
document.getElementById("minErrMax").value="30";
|
||
document.getElementById("abruptThreshold").value="200";
|
||
document.getElementById("smallThreshold").value="20";
|
||
document.getElementById("resetThreshold").value="500";
|
||
document.getElementById("peakProximity").value="0.05";
|
||
document.getElementById("cicleMax").value="3";
|
||
};
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<h2>Real-time Log</h2>
|
||
<div id="log" style="height:300px; overflow:auto; border:1px solid black; background:#f0f0f0; padding:5px;"></div>
|
||
|
||
<div class="container">
|
||
<div class="column">
|
||
<h2>Current Motor Positions</h2>
|
||
<p><strong>X:</strong> <span id="posXDisplay">0</span></p>
|
||
<p><strong>Y:</strong> <span id="posYDisplay">0</span></p>
|
||
<h2>Position Visualization</h2>
|
||
<canvas id="positionPlot" width="400" height="400" style="border:1px solid black;"></canvas>
|
||
</div>
|
||
|
||
<div class="column">
|
||
<h2>Motor Control</h2>
|
||
<div>
|
||
<h3>Coordinates (X, Y)</h3>
|
||
<input type="number" id="posX" placeholder="X (0 to 106000)" min="0" max="106000">
|
||
<input type="number" id="posY" placeholder="Y (0 to 104000)" min="0" max="104000">
|
||
<button onclick="setCoords()">Move to Coordinates</button>
|
||
<button onclick="setCurrentPos()">Set Current Position (from inputs)</button>
|
||
</div>
|
||
|
||
<div style="margin-top:10px;">
|
||
<h3>Fine-Tuning Parameters</h3>
|
||
<div class="param-group"><label>LPF Beta:</label> <input type="number" id="lpfBeta" step="0.01" min="0" max="1"> <button onclick="setLPFBeta()">Set</button></div>
|
||
<div class="param-group"><label>Error Max:</label> <input type="number" id="errMax" min="0"> <button onclick="setErrMax()">Set</button></div>
|
||
<div class="param-group"><label>Delay (s):</label> <input type="number" id="delayyy" min="0"> <button onclick="setDelay()">Set</button></div>
|
||
<div class="param-group"><label>Min Error Max:</label> <input type="number" id="minErrMax" min="0"> <button onclick="setMinErrMax()">Set</button></div>
|
||
<div class="param-group"><label>Abrupt Threshold:</label> <input type="number" id="abruptThreshold" min="0"> <button onclick="setAbruptThreshold()">Set</button></div>
|
||
<div class="param-group"><label>Small Threshold:</label> <input type="number" id="smallThreshold" min="0"> <button onclick="setSmallThreshold()">Set</button></div>
|
||
<div class="param-group"><label>Reset Threshold:</label> <input type="number" id="resetThreshold" min="0"> <button onclick="setResetThreshold()">Set</button></div>
|
||
<div class="param-group"><label>Peak Proximity (0-1):</label> <input type="number" id="peakProximity" step="0.01" min="0" max="1"> <button onclick="setPeakProximity()">Set</button></div>
|
||
<div class="param-group"><label>Cycle Max:</label> <input type="number" id="cicleMax" min="1"> <button onclick="setCicleMax()">Set</button></div>
|
||
</div>
|
||
|
||
<div style="margin-top:10px;">
|
||
<h3>Commands</h3>
|
||
<button onclick="sendCommand(2)">Track Sun</button>
|
||
<button onclick="sendCommand(3)">Fine-Tune</button>
|
||
<button onclick="sendCommand(4)">Set XY (Homing)</button>
|
||
<button onclick="sendCommand(5)">Move to Center</button>
|
||
<button onclick="sendCommand(6)">Set Current Position (legacy)</button>
|
||
<button onclick="sendCommand(10)">View Configuration</button>
|
||
<button onclick="sendCommand(11)">Demo Mode</button>
|
||
</div>
|
||
|
||
<div style="margin-top:10px;">
|
||
<h3>Auto-Tune (datos → umbrales)</h3>
|
||
<div class="param-group"><label><input type="checkbox" id="autoApply" onchange="toggleAutoApply()"> Auto-apply cada 60s</label></div>
|
||
<div class="param-group">
|
||
<button onclick="recomputeAT()">Recompute (solo log)</button>
|
||
<button onclick="applyAT()" style="margin-left:8px;">Apply Now</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>Firmware Update (OTA)</h2>
|
||
<form method="POST" action="/update" enctype="multipart/form-data">
|
||
<input type="file" name="update">
|
||
<input type="submit" value="Upload">
|
||
</form>
|
||
<h2>Options</h2>
|
||
<a href="/download" download="log.txt"><button>Download Log</button></a>
|
||
<a href="/clear"><button style="margin-left:10px;">Clear Log</button></a>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
)rawliteral";
|
||
server.send(200, "text/html", html);
|
||
}
|
||
|
||
// ==================== DESCARGA / LIMPIEZA / OTA ====================
|
||
void handleDownload() {
|
||
String txt;
|
||
for (int i = 0; i < MAX_LOG_SIZE; i++) {
|
||
int idx = (logIndex + i) % MAX_LOG_SIZE;
|
||
if (logBuffer[idx].length() > 0) txt += logBuffer[idx] + "\r\n";
|
||
}
|
||
server.send(200, "text/plain", txt);
|
||
}
|
||
|
||
void handleClearLog() {
|
||
limpiarLog();
|
||
server.sendHeader("Location", "/");
|
||
server.send(303);
|
||
}
|
||
|
||
void handleUpdateUpload() {
|
||
HTTPUpload& upload = server.upload();
|
||
if (upload.status == UPLOAD_FILE_START) {
|
||
if (!Update.begin()) Update.printError(Serial);
|
||
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial);
|
||
} else if (upload.status == UPLOAD_FILE_END) {
|
||
if (Update.end(true)) { Serial.printf("OTA completa: %u bytes\n", upload.totalSize); }
|
||
else { Update.printError(Serial); }
|
||
}
|
||
}
|
||
|
||
// ==================== ENDPOINTS DE SET ====================
|
||
void handleSetCoords() {
|
||
if (server.hasArg("x") && server.hasArg("y")) {
|
||
posX = -server.arg("x").toInt();
|
||
posY = -server.arg("y").toInt();
|
||
agregarLog("Recibi X:" + String(-posX) + ", Y:" + String(-posY));
|
||
stepperX.moveToPositionInSteps(posX);
|
||
stepperY.moveToPositionInSteps(posY);
|
||
sendPositionUpdate();
|
||
server.send(200, "text/plain", "Coordenadas seteadas");
|
||
} else server.send(400, "text/plain", "Faltan parámetros x o y");
|
||
}
|
||
void handleSetLPFBeta() { if (server.hasArg("beta")) { LPF_Beta = server.arg("beta").toFloat(); agregarLog("LPF_Beta="+String(LPF_Beta)); server.send(200,"text/plain","LPF_Beta OK"); } else server.send(400,"text/plain","Falta beta"); }
|
||
void handleSetErrMax() { if (server.hasArg("err")) { errmax = server.arg("err").toInt(); agregarLog("ErrMax="+String(errmax)); server.send(200,"text/plain","ErrMax OK"); } else server.send(400,"text/plain","Falta err"); }
|
||
void handleSetDelay() { if (server.hasArg("delay")){ delayyy = server.arg("delay").toInt(); agregarLog("Delay="+String(delayyy)); server.send(200,"text/plain","Delay OK"); } else server.send(400,"text/plain","Falta delay"); }
|
||
void handleSetMinErrMax() { if (server.hasArg("minErr")){min_errmax=server.arg("minErr").toInt(); agregarLog("MinErrMax="+String(min_errmax)); server.send(200,"text/plain","MinErrMax OK"); } else server.send(400,"text/plain","Falta minErr"); }
|
||
void handleSetAbruptThreshold(){ if (server.hasArg("abrupt")){abrupt_threshold=server.arg("abrupt").toInt(); agregarLog("AbruptThreshold="+String(abrupt_threshold)); server.send(200,"text/plain","AbruptThreshold OK"); } else server.send(400,"text/plain","Falta abrupt"); }
|
||
void handleSetSmallThreshold() { if (server.hasArg("small")) {small_threshold=server.arg("small").toInt(); agregarLog("SmallThreshold="+String(small_threshold)); server.send(200,"text/plain","SmallThreshold OK"); } else server.send(400,"text/plain","Falta small"); }
|
||
void handleSetResetThreshold() { if (server.hasArg("reset")) {reset_threshold=server.arg("reset").toInt(); agregarLog("ResetThreshold="+String(reset_threshold)); server.send(200,"text/plain","ResetThreshold OK"); } else server.send(400,"text/plain","Falta reset"); }
|
||
void handleSetPeakProximity() { if (server.hasArg("peak")) {peak_proximity=server.arg("peak").toFloat(); agregarLog("PeakProximity="+String(peak_proximity)); server.send(200,"text/plain","PeakProximity OK"); } else server.send(400,"text/plain","Falta peak"); }
|
||
void handleSetCicleMax() { if (server.hasArg("cicle")) {cicleMAX=server.arg("cicle").toInt(); agregarLog("CicleMax="+String(cicleMAX)); server.send(200,"text/plain","CicleMax OK"); } else server.send(400,"text/plain","Falta cicle"); }
|
||
|
||
// ==================== COMANDOS ====================
|
||
void handleCommand() {
|
||
if (!server.hasArg("status")) { server.send(400, "text/plain", "Falta status"); return; }
|
||
status = server.arg("status").toInt();
|
||
agregarLog("Ejecutando comando: " + String(status));
|
||
switch (status) {
|
||
case 2: trackSunByTime(); break;
|
||
case 3: status = 3; agregarLog("Iniciando Fine-Tune (nomegusta)"); break;
|
||
case 4: setXY(); sendPositionUpdate(); break;
|
||
case 5:
|
||
posX = LIMITX / 2; posY = LIMITY / 2;
|
||
stepperX.moveToPositionInSteps(posX); stepperY.moveToPositionInSteps(posY);
|
||
agregarLog("Moviendo a centro (X:" + String(-posX) + ", Y:" + String(-posY) + ")");
|
||
sendPositionUpdate(); break;
|
||
case 6: // Set current position (usa x/y si vienen)
|
||
if (server.hasArg("x") && server.hasArg("y")) {
|
||
posX = -server.arg("x").toInt();
|
||
posY = -server.arg("y").toInt();
|
||
stepperX.setCurrentPositionInSteps(posX);
|
||
stepperY.setCurrentPositionInSteps(posY);
|
||
stepper_driver_0.setMicrostepsPerStepPowerOfTwo(3);
|
||
stepper_driver_1.setMicrostepsPerStepPowerOfTwo(3);
|
||
stepperX.setSpeedInStepsPerSecond(4200); stepperX.setAccelerationInStepsPerSecondPerSecond(4000);
|
||
stepperY.setSpeedInStepsPerSecond(4200); stepperY.setAccelerationInStepsPerSecondPerSecond(4000);
|
||
agregarLog("Posición actual seteada: X:" + String(-posX) + ", Y:" + String(-posY));
|
||
sendPositionUpdate();
|
||
} else {
|
||
// si no vienen x/y, no fallamos: usamos los inputs previos (no hay en servidor), así que avisamos
|
||
agregarLog("status=6 sin x/y → Ignorado (usa botón 'Set Current Position (from inputs)')");
|
||
}
|
||
break;
|
||
case 10: {
|
||
TMC2209::Settings s = stepper_driver_0.getSettings();
|
||
agregarLog("Driver0: comm="+String(s.is_communicating)+", setup="+String(s.is_setup)+", en="+String(s.software_enabled)+", μsteps="+String(s.microsteps_per_step)+", irun="+String(s.irun_percent));
|
||
s = stepper_driver_1.getSettings();
|
||
agregarLog("Driver1: comm="+String(s.is_communicating)+", setup="+String(s.is_setup)+", en="+String(s.software_enabled)+", μsteps="+String(s.microsteps_per_step)+", irun="+String(s.irun_percent));
|
||
} break;
|
||
case 11:
|
||
setXY(); agregarLog("Modo demo activo, seteando");
|
||
stepperX.moveToPositionInSteps(LIMITX);
|
||
stepperY.moveToPositionInSteps(LIMITY);
|
||
agregarLog("Modo demo activo, moviendo al final (X:" + String(-posX) + ", Y:" + String(-posY) + ")"); sendPositionUpdate();
|
||
break;
|
||
default: server.send(400, "text/plain", "Comando no válido"); return;
|
||
}
|
||
server.send(200, "text/plain", "Comando ejecutado");
|
||
}
|
||
|
||
// ==================== AUTO-TUNE (recomendaciones) ====================
|
||
void at_recommend_thresholds(bool applyNow) {
|
||
if (atb.count < 20) { agregarLog("AT: pocos datos (" + String(atb.count) + "), espera más muestras"); return; }
|
||
|
||
const int n = atb.count;
|
||
static int32_t abs_dC[AT_CAP];
|
||
static int32_t dI[AT_CAP];
|
||
int m = 0;
|
||
int64_t sumI = 0, maxC = 0;
|
||
|
||
for (int k = 1; k < n; ++k) {
|
||
int i1 = at_index(atb.head - (n - 1 - k));
|
||
int i0 = at_index(atb.head - (n - 1 - (k - 1)));
|
||
int32_t dC = atb.C[i1] - atb.C[i0];
|
||
int32_t dIv = atb.I[i1] - atb.I[i0];
|
||
abs_dC[m] = abs(dC);
|
||
dI[m] = dIv;
|
||
m++;
|
||
}
|
||
for (int k = 0; k < n; ++k) {
|
||
int idx = at_index(atb.head - (n - 1 - k));
|
||
sumI += atb.I[idx];
|
||
if (llabs((long long)atb.C[idx]) > maxC) maxC = llabs((long long)atb.C[idx]);
|
||
}
|
||
float meanI = (float)sumI / (float)n;
|
||
|
||
// zona estable por irradiancia
|
||
const float thrI = max(25.0f, 0.03f * meanI);
|
||
static int32_t abs_dC_stable[AT_CAP];
|
||
int ms = 0;
|
||
for (int i = 0; i < m; ++i) if (fabs((float)dI[i]) <= thrI) abs_dC_stable[ms++] = abs_dC[i];
|
||
if (ms < 5) { for (int i = 0; i < m && i < 10; ++i) abs_dC_stable[i] = abs_dC[i]; ms = min(m, 10); }
|
||
|
||
int32_t q_small = (int32_t)lroundf(quantile_copy(abs_dC_stable, ms, 0.70f));
|
||
int32_t q_abrupt = (int32_t)lroundf(quantile_copy(abs_dC, m, 0.90f));
|
||
int32_t q_reset = (int32_t)lroundf(quantile_copy(abs_dC, m, 0.99f));
|
||
|
||
int32_t rec_small = max(5, q_small);
|
||
int32_t rec_abrupt = max(rec_small * 3, q_abrupt);
|
||
int32_t rec_reset = max(rec_abrupt * 2, q_reset);
|
||
|
||
float rec_peak = 0.05f;
|
||
if (maxC > 0) {
|
||
rec_peak = 2.0f * ((float)rec_small / (float)maxC);
|
||
if (rec_peak < 0.01f) rec_peak = 0.01f;
|
||
if (rec_peak > 0.10f) rec_peak = 0.10f;
|
||
}
|
||
|
||
char msg[180];
|
||
snprintf(msg, sizeof(msg),
|
||
"AT: meanI=%.1f, q70(dC_estable)=%d, q90(dC)=%d, q99(dC)=%d, max|C|=%lld",
|
||
meanI, q_small, q_abrupt, q_reset, (long long)maxC);
|
||
agregarLog(msg);
|
||
|
||
snprintf(msg, sizeof(msg),
|
||
"AT: rec small=%d, abrupt=%d, reset=%d, peak_prox=%.3f",
|
||
rec_small, rec_abrupt, rec_reset, rec_peak);
|
||
agregarLog(msg);
|
||
|
||
if (applyNow) {
|
||
small_threshold = rec_small;
|
||
abrupt_threshold = rec_abrupt;
|
||
reset_threshold = rec_reset;
|
||
peak_proximity = rec_peak;
|
||
snprintf(msg, sizeof(msg),
|
||
"AT: APLICADOS -> small=%d, abrupt=%d, reset=%d, peak=%.3f",
|
||
small_threshold, abrupt_threshold, reset_threshold, peak_proximity);
|
||
agregarLog(msg);
|
||
}
|
||
}
|
||
|
||
// Endpoints Auto-Tune
|
||
void handleRecomputeAutoTune() { at_recommend_thresholds(false); server.send(200, "text/plain", "Recomputed (ver log)"); }
|
||
void handleApplyAutoTune() { at_recommend_thresholds(true); server.send(200, "text/plain", "Applied (ver log)"); }
|
||
void handleSetAutoApply() {
|
||
if (server.hasArg("on")) {
|
||
at_auto_apply = (server.arg("on") == "1");
|
||
agregarLog(String("AT auto-apply: ") + (at_auto_apply ? "ON" : "OFF"));
|
||
server.send(200, "text/plain", "OK");
|
||
} else server.send(400, "text/plain", "Falta 'on'");
|
||
}
|
||
|
||
// ==================== SETUP ====================
|
||
void setup() {
|
||
Serial.begin(9600);
|
||
serial_stream.begin(SERIAL_BAUD_RATE);
|
||
|
||
WiFi.begin(ssid, password);
|
||
Serial.print("Conectando a WiFi");
|
||
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
|
||
Serial.println("\nWiFi conectado.");
|
||
Serial.print("IP local: "); Serial.println(WiFi.localIP());
|
||
agregarLog("WiFi conectado. IP: " + WiFi.localIP().toString());
|
||
|
||
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
|
||
struct tm timeinfo;
|
||
if (!getLocalTime(&timeinfo)) { Serial.println("Error al sincronizar NTP."); agregarLog("Error al sincronizar NTP."); }
|
||
else { Serial.println(&timeinfo, "Hora sincronizada: %Y-%m-%d %H:%M:%S"); agregarLog("Hora sincronizada"); }
|
||
|
||
pinMode(10, OUTPUT); digitalWrite(10, LOW);
|
||
stepper_driver_0.setup(serial_stream, SERIAL_BAUD_RATE, SERIAL_ADDRESS_1);
|
||
stepper_driver_0.setReplyDelay(REPLY_DELAY);
|
||
stepper_driver_1.setup(serial_stream, SERIAL_BAUD_RATE, SERIAL_ADDRESS_3);
|
||
stepper_driver_1.setReplyDelay(REPLY_DELAY);
|
||
|
||
while (!stepper_driver_0.isCommunicating()) {
|
||
Serial.println("Stepper driver 1 not setup and communicating!");
|
||
agregarLog("Stepper driver 1 no configurado");
|
||
stepper_driver_0.setup(serial_stream, SERIAL_BAUD_RATE, SERIAL_ADDRESS_1);
|
||
stepper_driver_0.setReplyDelay(REPLY_DELAY);
|
||
delay(100);
|
||
}
|
||
Serial.println("COM READY"); agregarLog("COM READY");
|
||
|
||
stepper_driver_0.moveAtVelocity(0);
|
||
stepper_driver_0.setRunCurrent(RUN_CURRENT_PERCENT0);
|
||
stepper_driver_0.setMicrostepsPerStepPowerOfTwo(3);
|
||
stepper_driver_0.setStallGuardThreshold(STALL_GUARD_THRESHOLD0);
|
||
stepper_driver_0.enableAutomaticCurrentScaling();
|
||
stepper_driver_0.enableCoolStep(0, 2);
|
||
stepper_driver_0.enable();
|
||
stepper_driver_0.setHoldCurrent(2);
|
||
|
||
stepper_driver_1.moveAtVelocity(0);
|
||
stepper_driver_1.setRunCurrent(RUN_CURRENT_PERCENT1);
|
||
stepper_driver_1.setMicrostepsPerStepPowerOfTwo(3);
|
||
stepper_driver_1.setStallGuardThreshold(STALL_GUARD_THRESHOLD1);
|
||
stepper_driver_1.enableAutomaticCurrentScaling();
|
||
stepper_driver_1.enableCoolStep(0, 2);
|
||
stepper_driver_1.enable();
|
||
stepper_driver_1.setHoldCurrent(2);
|
||
|
||
stepperX.connectToPins(MOTOR_X_STEP_PIN, MOTOR_X_DIRECTION_PIN);
|
||
stepperY.connectToPins(MOTOR_Y_STEP_PIN, MOTOR_Y_DIRECTION_PIN);
|
||
stepperX.setSpeedInStepsPerSecond(4200);
|
||
stepperX.setAccelerationInStepsPerSecondPerSecond(4000);
|
||
stepperY.setSpeedInStepsPerSecond(4200);
|
||
stepperY.setAccelerationInStepsPerSecondPerSecond(4000);
|
||
|
||
pinMode(9, OUTPUT);
|
||
digitalWrite(9, VocIsc);
|
||
externalADC_init();
|
||
|
||
server.on("/", HTTP_GET, handleRoot);
|
||
server.on("/download", HTTP_GET, handleDownload);
|
||
server.on("/clear", HTTP_GET, handleClearLog);
|
||
server.on("/setCoords", HTTP_GET, handleSetCoords);
|
||
server.on("/setLPFBeta", HTTP_GET, handleSetLPFBeta);
|
||
server.on("/setErrMax", HTTP_GET, handleSetErrMax);
|
||
server.on("/setDelay", HTTP_GET, handleSetDelay);
|
||
server.on("/setMinErrMax", HTTP_GET, handleSetMinErrMax);
|
||
server.on("/setAbruptThreshold", HTTP_GET, handleSetAbruptThreshold);
|
||
server.on("/setSmallThreshold", HTTP_GET, handleSetSmallThreshold);
|
||
server.on("/setResetThreshold", HTTP_GET, handleSetResetThreshold);
|
||
server.on("/setPeakProximity", HTTP_GET, handleSetPeakProximity);
|
||
server.on("/setCicleMax", HTTP_GET, handleSetCicleMax);
|
||
server.on("/command", HTTP_GET, handleCommand);
|
||
server.on("/update", HTTP_POST,
|
||
[](){ server.sendHeader("Connection", "close"); server.send(200, "text/plain", Update.hasError() ? "Fallo OTA" : "OTA OK, reiniciando..."); ESP.restart(); },
|
||
handleUpdateUpload
|
||
);
|
||
// Auto-Tune endpoints
|
||
server.on("/recomputeAutoTune", HTTP_GET, handleRecomputeAutoTune);
|
||
server.on("/applyAutoTune", HTTP_GET, handleApplyAutoTune);
|
||
server.on("/setAutoApply", HTTP_GET, handleSetAutoApply);
|
||
|
||
server.begin();
|
||
iniciarWebSocket();
|
||
agregarLog("Servidor iniciado como cliente WiFi.");
|
||
|
||
Serial.println("Free heap: " + String(ESP.getFreeHeap()));
|
||
}
|
||
|
||
// ==================== LOOP ====================
|
||
void loop() {
|
||
server.handleClient();
|
||
webSocket.loop();
|
||
|
||
static int32_t lastPosX = posX, lastPosY = posY;
|
||
|
||
if (millis() / 1000 >= tatras + delayyy) {
|
||
leetodo();
|
||
tatras = millis() / 1000;
|
||
|
||
char buff[90];
|
||
sprintf(buff, "X:%d,Y:%d,C:%d,V:%d,S:%d,er:%d", -posX, -posY, rawval[1], rawval[3], tatras, error);
|
||
agregarLog(buff);
|
||
|
||
// push al buffer de Auto-Tune (Irradiancia desde V2 ajustada)
|
||
float v2_base = ((float)rawval[2] / 8388607.0f) * 2048.0f;
|
||
int32_t I_wm2 = (int32_t)lroundf(v2_base / 2.0f);
|
||
int32_t C_counts = (int32_t)rawval[1];
|
||
at_push(C_counts, I_wm2, millis());
|
||
|
||
int32_t varerr = -(int32_t)rawval[1];
|
||
error = errmax; // nomegusta ajusta dinámicamente
|
||
if (status == 3) nomegusta(varerr, error);
|
||
}
|
||
|
||
if (posX != lastPosX || posY != lastPosY) {
|
||
sendPositionUpdate();
|
||
lastPosX = posX; lastPosY = posY;
|
||
}
|
||
|
||
// Auto-apply periódico
|
||
if (at_auto_apply && (millis() - at_last_apply_ms >= AT_PERIOD_MS)) {
|
||
at_recommend_thresholds(true);
|
||
at_last_apply_ms = millis();
|
||
}
|
||
}
|